Compare commits

..

1 Commits

Author SHA1 Message Date
Jonathan Ervine
2d0be883da Remove commented out MySqlDB commands 2021-09-20 07:15:44 +08:00
101 changed files with 71 additions and 11901 deletions

View File

@ -1,133 +0,0 @@
# Git
.git
.gitignore
.gitattributes
# Documentation
README.md
*.md
docs/
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
venv/
env/
ENV/
env.bak/
venv.bak/
.venv/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
*.log
logs/
# Database files (will be created in container)
*.db
*.sqlite
*.sqlite3
# Data directories
data/
temp/
tmp/
# Docker
Dockerfile*
docker-compose*.yml
.dockerignore
# Environment files
.env
.env.local
.env.production
.env.development
# Node modules (if any)
node_modules/
# Coverage reports
htmlcov/
.coverage
.coverage.*
coverage.xml
# Testing
.pytest_cache/
.tox/
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# pipenv
Pipfile.lock
# PEP 582
__pypackages__/
# Celery
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json

View File

@ -1,76 +0,0 @@
# Multi-stage Containerfile for Hockey Results Application
# Supports multiple database backends (PostgreSQL, MariaDB, SQLite)
# Stage 1: Build stage
FROM python:3.11-slim as builder
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Install system dependencies for building Python packages
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
default-libmysqlclient-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
# Create and activate virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Copy requirements and install Python dependencies
COPY requirements.txt motm_app/requirements.txt ./
RUN pip install --upgrade pip && \
pip install -r requirements.txt && \
pip install -r motm_app/requirements.txt
# Stage 2: Runtime stage
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH" \
DATABASE_TYPE=sqlite \
FLASK_ENV=production \
FLASK_APP=motm_app/main.py
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
libpq5 \
default-mysql-client \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy virtual environment from builder stage
COPY --from=builder /opt/venv /opt/venv
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Create application directory
WORKDIR /app
# Copy application code
COPY --chown=appuser:appuser . .
# Create directories for data and logs
RUN mkdir -p /app/data /app/logs && \
chown -R appuser:appuser /app/data /app/logs
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 5000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:5000/ || exit 1
# Default command
CMD ["python", "motm_app/main.py"]

362
DOCKER.md
View File

@ -1,362 +0,0 @@
# Docker Containerization Guide
This guide explains how to run the Hockey Results Application using Docker containers with support for multiple database backends.
## 🏒 Quick Start
### Using Docker Compose (Recommended)
```bash
# Start with PostgreSQL (default)
docker-compose up -d
# Start with MariaDB
docker-compose --profile mariadb up -d
# Start with SQLite (no database container needed)
DATABASE_TYPE=sqlite docker-compose up -d
```
### Using Docker Commands
```bash
# Build the image
./docker/build.sh
# Run with SQLite
./docker/run.sh
# Run with PostgreSQL
./docker/run.sh -d postgresql
# Run with MariaDB
./docker/run.sh -d mysql
```
## 🗄️ Database Options
### 1. SQLite (Default)
- **Pros**: No external dependencies, perfect for development
- **Cons**: Not suitable for production with multiple users
- **Use case**: Development, testing, single-user deployments
```bash
# Using docker-compose
DATABASE_TYPE=sqlite docker-compose up -d
# Using docker run
./docker/run.sh -d sqlite
```
### 2. PostgreSQL (Recommended for Production)
- **Pros**: Robust, ACID compliant, excellent performance
- **Cons**: Requires external database container
- **Use case**: Production deployments, multi-user applications
```bash
# Using docker-compose (default)
docker-compose up -d
# Using docker run (requires external PostgreSQL)
./docker/run.sh -d postgresql
```
### 3. MariaDB/MySQL
- **Pros**: Widely supported, good performance
- **Cons**: Requires external database container
- **Use case**: Legacy systems, specific MySQL requirements
```bash
# Using docker-compose
docker-compose --profile mariadb up -d
# Using docker run (requires external MariaDB)
./docker/run.sh -d mysql
```
## 🚀 Deployment Options
### Development Environment
```bash
# Clone the repository
git clone <repository-url>
cd gcp-hockey-results
# Start with SQLite for development
./docker/run.sh -d sqlite -p 5000:5000
# Access the application
open http://localhost:5000
```
### Production Environment
#### Option 1: Docker Compose (Single Server)
```bash
# Set production environment variables
export SECRET_KEY="your-production-secret-key"
export BASIC_AUTH_PASSWORD="strong-production-password"
export POSTGRES_PASSWORD="strong-database-password"
# Start production stack
docker-compose up -d
# Scale the application (optional)
docker-compose up -d --scale hockey-app=3
```
#### Option 2: Kubernetes (Multi-Server)
```yaml
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: hockey-app
spec:
replicas: 3
selector:
matchLabels:
app: hockey-app
template:
metadata:
labels:
app: hockey-app
spec:
containers:
- name: hockey-app
image: hockey-results:latest
ports:
- containerPort: 5000
env:
- name: DATABASE_TYPE
value: "postgresql"
- name: POSTGRES_HOST
value: "postgres-service"
```
#### Option 3: Cloud Platforms
**Google Cloud Run:**
```bash
# Build and push to Google Container Registry
./docker/build.sh --registry gcr.io/your-project-id --push
# Deploy to Cloud Run
gcloud run deploy hockey-app \
--image gcr.io/your-project-id/hockey-results:latest \
--platform managed \
--region us-central1 \
--set-env-vars DATABASE_TYPE=postgresql
```
**AWS ECS:**
```bash
# Build and push to ECR
./docker/build.sh --registry your-account.dkr.ecr.region.amazonaws.com --push
# Create ECS task definition with the image
```
## 🔧 Configuration
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `DATABASE_TYPE` | `sqlite` | Database type: sqlite, postgresql, mysql |
| `SECRET_KEY` | `your-secret-key-change-in-production` | Flask secret key |
| `FLASK_ENV` | `production` | Flask environment |
| `BASIC_AUTH_USERNAME` | `admin` | Basic auth username |
| `BASIC_AUTH_PASSWORD` | `letmein` | Basic auth password |
### PostgreSQL Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `POSTGRES_HOST` | `postgres` | PostgreSQL host |
| `POSTGRES_PORT` | `5432` | PostgreSQL port |
| `POSTGRES_DATABASE` | `hockey_results` | Database name |
| `POSTGRES_USER` | `hockey_user` | Database user |
| `POSTGRES_PASSWORD` | `hockey_password` | Database password |
### MySQL/MariaDB Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `MYSQL_HOST` | `mariadb` | MySQL host |
| `MYSQL_PORT` | `3306` | MySQL port |
| `MYSQL_DATABASE` | `hockey_results` | Database name |
| `MYSQL_USER` | `hockey_user` | Database user |
| `MYSQL_PASSWORD` | `hockey_password` | Database password |
## 📊 Monitoring and Logs
### View Logs
```bash
# Docker Compose
docker-compose logs -f hockey-app
# Docker Run
docker logs -f hockey-results-app
```
### Health Checks
```bash
# Check container health
docker ps
# Manual health check
curl -f http://localhost:5000/ || echo "Application is not healthy"
```
### Database Connection Test
```bash
# Test PostgreSQL connection
docker exec hockey-results-app python -c "
from motm_app.database import db_config
print('Database URL:', db_config.database_url)
"
# Test application database initialization
docker exec hockey-results-app python -c "
from motm_app.database import init_database
init_database()
print('Database initialized successfully')
"
```
## 🛠️ Development
### Local Development with Docker
```bash
# Build development image
./docker/build.sh -t dev
# Run with volume mount for live code changes
docker run -it --rm \
-p 5000:5000 \
-v $(pwd):/app \
-e FLASK_ENV=development \
-e FLASK_DEBUG=true \
hockey-results:dev
```
### Database Migrations
```bash
# Run migrations inside container
docker exec hockey-results-app python -c "
from motm_app.database import init_database
init_database()
"
```
### Backup and Restore
```bash
# Backup PostgreSQL data
docker exec hockey-postgres pg_dump -U hockey_user hockey_results > backup.sql
# Restore PostgreSQL data
docker exec -i hockey-postgres psql -U hockey_user hockey_results < backup.sql
# Backup SQLite data
docker cp hockey-results-app:/app/data/hockey_results.db ./backup.db
```
## 🔒 Security Considerations
### Production Security
1. **Change default passwords**:
```bash
export SECRET_KEY="$(openssl rand -hex 32)"
export BASIC_AUTH_PASSWORD="$(openssl rand -base64 32)"
export POSTGRES_PASSWORD="$(openssl rand -base64 32)"
```
2. **Use secrets management**:
```yaml
# docker-compose.prod.yml
services:
hockey-app:
environment:
- SECRET_KEY_FILE=/run/secrets/secret_key
secrets:
- secret_key
secrets:
secret_key:
file: ./secrets/secret_key.txt
```
3. **Network security**:
```yaml
# Use custom networks
networks:
hockey-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
```
## 🚨 Troubleshooting
### Common Issues
**Container won't start:**
```bash
# Check logs
docker logs hockey-results-app
# Check if port is already in use
netstat -tlnp | grep :5000
```
**Database connection issues:**
```bash
# Check database container status
docker-compose ps
# Test database connectivity
docker exec hockey-results-app python -c "
import psycopg2
conn = psycopg2.connect(host='postgres', port=5432, database='hockey_results', user='hockey_user', password='hockey_password')
print('Database connected successfully')
"
```
**Permission issues:**
```bash
# Fix file permissions
sudo chown -R $USER:$USER ./data
chmod -R 755 ./data
```
### Performance Tuning
**Database Optimization:**
```bash
# PostgreSQL tuning
docker exec hockey-postgres psql -U hockey_user hockey_results -c "
ALTER SYSTEM SET shared_buffers = '256MB';
ALTER SYSTEM SET effective_cache_size = '1GB';
SELECT pg_reload_conf();
"
```
**Application Scaling:**
```bash
# Scale application containers
docker-compose up -d --scale hockey-app=3
# Use load balancer
docker-compose --profile nginx up -d
```
## 📚 Additional Resources
- [Docker Documentation](https://docs.docker.com/)
- [Docker Compose Documentation](https://docs.docker.com/compose/)
- [PostgreSQL Docker Image](https://hub.docker.com/_/postgres)
- [MariaDB Docker Image](https://hub.docker.com/_/mariadb)
- [SQLAlchemy Documentation](https://docs.sqlalchemy.org/)

View File

@ -21,6 +21,7 @@ libraries:
env_variables:
CLOUDSQL_CONNECTION_NAME: hk-hockey:asia-east2:hk-hockey-sql
LOCAL_DB_SERVER: mariadb.db.svc.cluster.local
CLOUDSQL_USER: root
CLOUDSQL_WRITE_USER: hockeyWrite
CLOUDSQL_READ_USER: hockeyRead

View File

@ -26,8 +26,6 @@ def write_cloudsql():
# Connect using the unix socket located at
# /cloudsql/cloudsql-connection-name.
cloudsql_unix_socket = os.path.join('/cloudsql', CLOUDSQL_CONNECTION_NAME)
# db = MySQLdb.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=CLOUDSQL_DATABASE, charset=CLOUDSQL_CHARSET)
db = pymysql.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=CLOUDSQL_DATABASE, charset=CLOUDSQL_CHARSET)
# If the unix socket is unavailable, then try to connect using TCP. This
@ -37,7 +35,6 @@ def write_cloudsql():
# $ cloud_sql_proxy -instances=your-connection-name=tcp:3306
#
else:
# db = MySQLdb.connect(host='db.ipa.champion', user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=CLOUDSQL_DATABASE, charset=CLOUDSQL_CHARSET)
db = pymysql.connect(host=LOCAL_DB_SERVER, user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=LOCAL_DATABASE, charset=CLOUDSQL_CHARSET)
return db
@ -48,8 +45,6 @@ def write_cloudsql_static():
# Connect using the unix socket located at
# /cloudsql/cloudsql-connection-name.
cloudsql_unix_socket = os.path.join('/cloudsql', CLOUDSQL_CONNECTION_NAME)
# db = MySQLdb.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
db = pymysql.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
# If the unix socket is unavailable, then try to connect using TCP. This
@ -59,7 +54,6 @@ def write_cloudsql_static():
# $ cloud_sql_proxy -instances=your-connection-name=tcp:3306
#
else:
# db = MySQLdb.connect(host='db.ipa.champion', user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
db = pymysql.connect(host=LOCAL_DB_SERVER, user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
return db
@ -70,8 +64,6 @@ def read_cloudsql():
# Connect using the unix socket located at
# /cloudsql/cloudsql-connection-name.
cloudsql_unix_socket = os.path.join('/cloudsql', CLOUDSQL_CONNECTION_NAME)
# db = MySQLdb.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=CLOUDSQL_DATABASE, charset=CLOUDSQL_CHARSET)
db = pymysql.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=CLOUDSQL_DATABASE, charset=CLOUDSQL_CHARSET)
# If the unix socket is unavailable, then try to connect using TCP. This
@ -81,7 +73,6 @@ def read_cloudsql():
# $ cloud_sql_proxy -instances=your-connection-name=tcp:3306
#
else:
# db = MySQLdb.connect(host='db.ipa.champion', user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=CLOUDSQL_DATABASE, charset=CLOUDSQL_CHARSET)
db = pymysql.connect(host=LOCAL_DB_SERVER, user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=LOCAL_DATABASE, charset=CLOUDSQL_CHARSET)
return db
@ -92,8 +83,6 @@ def read_cloudsql_static():
# Connect using the unix socket located at
# /cloudsql/cloudsql-connection-name.
cloudsql_unix_socket = os.path.join('/cloudsql', CLOUDSQL_CONNECTION_NAME)
# db = MySQLdb.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
db = pymysql.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
# If the unix socket is unavailable, then try to connect using TCP. This
@ -103,14 +92,12 @@ def read_cloudsql_static():
# $ cloud_sql_proxy -instances=your-connection-name=tcp:3306
#
else:
# db = MySQLdb.connect(host='db.ipa.champion', user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
db = pymysql.connect(host=LOCAL_DB_SERVER, user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
return db
def sql_write(sql_cmd):
try:
db = write_cloudsql()
# cursor = db.cursor(MySQLdb.cursors.DictCursor)
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute(sql_cmd)
db.commit()
@ -124,7 +111,6 @@ def sql_write(sql_cmd):
def sql_write_static(sql_cmd):
try:
db = write_cloudsql_static()
# cursor = db.cursor(MySQLdb.cursors.DictCursor)
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute(sql_cmd)
db.commit()
@ -138,7 +124,6 @@ def sql_write_static(sql_cmd):
def sql_read(sql_cmd):
try:
db = read_cloudsql()
# cursor = db.cursor(MySQLdb.cursors.DictCursor)
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute(sql_cmd)
rows = cursor.fetchall()
@ -153,7 +138,6 @@ def sql_read(sql_cmd):
def sql_read_static(sql_cmd):
try:
db = read_cloudsql_static()
# cursor = db.cursor(MySQLdb.cursors.DictCursor)
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute(sql_cmd)
rows = cursor.fetchall()

View File

@ -1,125 +0,0 @@
version: '3.8'
services:
# Hockey Results Application
hockey-app:
build:
context: .
dockerfile: Containerfile
container_name: hockey-results-app
ports:
- "5000:5000"
environment:
- DATABASE_TYPE=postgresql
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- POSTGRES_DATABASE=hockey_results
- POSTGRES_USER=hockey_user
- POSTGRES_PASSWORD=hockey_password
- FLASK_ENV=production
- SECRET_KEY=your-secret-key-change-in-production
- BASIC_AUTH_USERNAME=admin
- BASIC_AUTH_PASSWORD=letmein
volumes:
- ./data:/app/data
- ./logs:/app/logs
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
networks:
- hockey-network
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: hockey-postgres
environment:
- POSTGRES_DB=hockey_results
- POSTGRES_USER=hockey_user
- POSTGRES_PASSWORD=hockey_password
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U hockey_user -d hockey_results"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- hockey-network
# MariaDB Database (Alternative)
mariadb:
image: mariadb:10.11
container_name: hockey-mariadb
environment:
- MYSQL_ROOT_PASSWORD=root_password
- MYSQL_DATABASE=hockey_results
- MYSQL_USER=hockey_user
- MYSQL_PASSWORD=hockey_password
- MYSQL_CHARSET=utf8mb4
- MYSQL_COLLATION=utf8mb4_unicode_ci
volumes:
- mariadb_data:/var/lib/mysql
- ./init-scripts:/docker-entrypoint-initdb.d
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "hockey_user", "-p$$MYSQL_PASSWORD"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- hockey-network
profiles:
- mariadb
# Redis for caching (optional)
redis:
image: redis:7-alpine
container_name: hockey-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
restart: unless-stopped
networks:
- hockey-network
profiles:
- redis
# Nginx reverse proxy (optional)
nginx:
image: nginx:alpine
container_name: hockey-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- hockey-app
restart: unless-stopped
networks:
- hockey-network
profiles:
- nginx
volumes:
postgres_data:
driver: local
mariadb_data:
driver: local
redis_data:
driver: local
networks:
hockey-network:
driver: bridge

View File

@ -1,127 +0,0 @@
#!/bin/bash
# Docker build script for Hockey Results Application
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
IMAGE_NAME="hockey-results"
IMAGE_TAG="latest"
FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}"
echo -e "${BLUE}🏒 Building Hockey Results Application Docker Image${NC}"
echo -e "${BLUE}================================================${NC}"
# Function to display usage
usage() {
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -t, --tag TAG Set image tag (default: latest)"
echo " -n, --name NAME Set image name (default: hockey-results)"
echo " --no-cache Build without cache"
echo " --push Push image to registry after build"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " $0 # Build with default settings"
echo " $0 -t v1.0.0 # Build with specific tag"
echo " $0 --no-cache # Build without cache"
echo " $0 -t v1.0.0 --push # Build and push to registry"
}
# Parse command line arguments
PUSH_IMAGE=false
NO_CACHE=""
REGISTRY=""
while [[ $# -gt 0 ]]; do
case $1 in
-t|--tag)
IMAGE_TAG="$2"
FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}"
shift 2
;;
-n|--name)
IMAGE_NAME="$2"
FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}"
shift 2
;;
--no-cache)
NO_CACHE="--no-cache"
shift
;;
--push)
PUSH_IMAGE=true
shift
;;
--registry)
REGISTRY="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
usage
exit 1
;;
esac
done
echo -e "${YELLOW}📋 Build Configuration:${NC}"
echo -e " Image Name: ${GREEN}${FULL_IMAGE_NAME}${NC}"
echo -e " No Cache: ${GREEN}${NO_CACHE:-false}${NC}"
echo -e " Push Image: ${GREEN}${PUSH_IMAGE}${NC}"
echo ""
# Check if Docker is running
if ! docker info > /dev/null 2>&1; then
echo -e "${RED}❌ Docker is not running. Please start Docker and try again.${NC}"
exit 1
fi
# Build the image
echo -e "${BLUE}🔨 Building Docker image...${NC}"
if docker build $NO_CACHE -t "$FULL_IMAGE_NAME" .; then
echo -e "${GREEN}✅ Docker image built successfully: ${FULL_IMAGE_NAME}${NC}"
else
echo -e "${RED}❌ Docker build failed${NC}"
exit 1
fi
# Show image information
echo -e "${BLUE}📊 Image Information:${NC}"
docker images "$FULL_IMAGE_NAME"
# Push image if requested
if [ "$PUSH_IMAGE" = true ]; then
echo -e "${BLUE}📤 Pushing image to registry...${NC}"
if [ -n "$REGISTRY" ]; then
FULL_REGISTRY_NAME="${REGISTRY}/${FULL_IMAGE_NAME}"
docker tag "$FULL_IMAGE_NAME" "$FULL_REGISTRY_NAME"
docker push "$FULL_REGISTRY_NAME"
echo -e "${GREEN}✅ Image pushed to registry: ${FULL_REGISTRY_NAME}${NC}"
else
docker push "$FULL_IMAGE_NAME"
echo -e "${GREEN}✅ Image pushed to registry: ${FULL_IMAGE_NAME}${NC}"
fi
fi
echo -e "${GREEN}🎉 Build process completed successfully!${NC}"
# Show next steps
echo -e "${YELLOW}📝 Next Steps:${NC}"
echo -e " To run the container: ${BLUE}docker run -p 5000:5000 ${FULL_IMAGE_NAME}${NC}"
echo -e " To run with docker-compose: ${BLUE}docker-compose up${NC}"
echo -e " To run with PostgreSQL: ${BLUE}docker-compose --profile postgres up${NC}"
echo -e " To run with MariaDB: ${BLUE}docker-compose --profile mariadb up${NC}"

View File

@ -1,192 +0,0 @@
#!/bin/bash
# Docker run script for Hockey Results Application
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default configuration
IMAGE_NAME="hockey-results:latest"
CONTAINER_NAME="hockey-results-app"
PORT="5000:5000"
DATABASE_TYPE="sqlite"
VOLUME_MOUNT=""
# Function to display usage
usage() {
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -i, --image IMAGE Docker image name (default: hockey-results:latest)"
echo " -c, --container NAME Container name (default: hockey-results-app)"
echo " -p, --port PORT Port mapping (default: 5000:5000)"
echo " -d, --database TYPE Database type: sqlite, postgresql, mysql (default: sqlite)"
echo " -v, --volume PATH Mount volume for data persistence"
echo " --detach Run container in background"
echo " --rm Remove container when it exits"
echo " --env KEY=VALUE Set environment variable"
echo " -h, --help Show this help message"
echo ""
echo "Database Types:"
echo " sqlite Use SQLite database (default, no external dependencies)"
echo " postgresql Use PostgreSQL database"
echo " mysql Use MySQL/MariaDB database"
echo ""
echo "Examples:"
echo " $0 # Run with SQLite"
echo " $0 -d postgresql # Run with PostgreSQL"
echo " $0 -v ./data:/app/data # Run with data persistence"
echo " $0 -p 8080:5000 # Run on different port"
echo " $0 --detach # Run in background"
}
# Parse command line arguments
DETACH=""
REMOVE=""
ENV_VARS=""
while [[ $# -gt 0 ]]; do
case $1 in
-i|--image)
IMAGE_NAME="$2"
shift 2
;;
-c|--container)
CONTAINER_NAME="$2"
shift 2
;;
-p|--port)
PORT="$2"
shift 2
;;
-d|--database)
DATABASE_TYPE="$2"
shift 2
;;
-v|--volume)
VOLUME_MOUNT="-v $2"
shift 2
;;
--detach)
DETACH="-d"
shift
;;
--rm)
REMOVE="--rm"
shift
;;
--env)
ENV_VARS="$ENV_VARS -e $2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
usage
exit 1
;;
esac
done
echo -e "${BLUE}🏒 Running Hockey Results Application${NC}"
echo -e "${BLUE}====================================${NC}"
# Check if Docker is running
if ! docker info > /dev/null 2>&1; then
echo -e "${RED}❌ Docker is not running. Please start Docker and try again.${NC}"
exit 1
fi
# Check if image exists
if ! docker images -q "$IMAGE_NAME" | grep -q .; then
echo -e "${YELLOW}⚠️ Image $IMAGE_NAME not found. Building it first...${NC}"
./docker/build.sh -n "$(echo $IMAGE_NAME | cut -d':' -f1)" -t "$(echo $IMAGE_NAME | cut -d':' -f2)"
fi
# Stop existing container if running
if docker ps -q -f name="$CONTAINER_NAME" | grep -q .; then
echo -e "${YELLOW}🛑 Stopping existing container...${NC}"
docker stop "$CONTAINER_NAME"
fi
# Remove existing container if it exists
if docker ps -aq -f name="$CONTAINER_NAME" | grep -q .; then
echo -e "${YELLOW}🗑️ Removing existing container...${NC}"
docker rm "$CONTAINER_NAME"
fi
# Set environment variables based on database type
case $DATABASE_TYPE in
"postgresql")
ENV_VARS="$ENV_VARS -e DATABASE_TYPE=postgresql"
ENV_VARS="$ENV_VARS -e POSTGRES_HOST=host.docker.internal"
ENV_VARS="$ENV_VARS -e POSTGRES_PORT=5432"
ENV_VARS="$ENV_VARS -e POSTGRES_DATABASE=hockey_results"
ENV_VARS="$ENV_VARS -e POSTGRES_USER=hockey_user"
ENV_VARS="$ENV_VARS -e POSTGRES_PASSWORD=hockey_password"
echo -e "${YELLOW}📊 Using PostgreSQL database${NC}"
;;
"mysql")
ENV_VARS="$ENV_VARS -e DATABASE_TYPE=mysql"
ENV_VARS="$ENV_VARS -e MYSQL_HOST=host.docker.internal"
ENV_VARS="$ENV_VARS -e MYSQL_PORT=3306"
ENV_VARS="$ENV_VARS -e MYSQL_DATABASE=hockey_results"
ENV_VARS="$ENV_VARS -e MYSQL_USER=hockey_user"
ENV_VARS="$ENV_VARS -e MYSQL_PASSWORD=hockey_password"
echo -e "${YELLOW}📊 Using MySQL/MariaDB database${NC}"
;;
"sqlite")
ENV_VARS="$ENV_VARS -e DATABASE_TYPE=sqlite"
ENV_VARS="$ENV_VARS -e SQLITE_DATABASE_PATH=/app/data/hockey_results.db"
VOLUME_MOUNT="$VOLUME_MOUNT -v $(pwd)/data:/app/data"
echo -e "${YELLOW}📊 Using SQLite database${NC}"
;;
*)
echo -e "${RED}❌ Unsupported database type: $DATABASE_TYPE${NC}"
echo -e "Supported types: sqlite, postgresql, mysql"
exit 1
;;
esac
echo -e "${BLUE}🚀 Starting container...${NC}"
echo -e " Image: ${GREEN}$IMAGE_NAME${NC}"
echo -e " Container: ${GREEN}$CONTAINER_NAME${NC}"
echo -e " Port: ${GREEN}$PORT${NC}"
echo -e " Database: ${GREEN}$DATABASE_TYPE${NC}"
# Run the container
docker run \
$DETACH \
$REMOVE \
--name "$CONTAINER_NAME" \
-p "$PORT" \
$VOLUME_MOUNT \
$ENV_VARS \
"$IMAGE_NAME"
if [ $? -eq 0 ]; then
if [ -n "$DETACH" ]; then
echo -e "${GREEN}✅ Container started successfully in background${NC}"
echo -e "${BLUE}📝 Container Information:${NC}"
docker ps -f name="$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
echo ""
echo -e "${YELLOW}📋 Useful Commands:${NC}"
echo -e " View logs: ${BLUE}docker logs $CONTAINER_NAME${NC}"
echo -e " Stop container: ${BLUE}docker stop $CONTAINER_NAME${NC}"
echo -e " Remove container: ${BLUE}docker rm $CONTAINER_NAME${NC}"
echo -e " Access shell: ${BLUE}docker exec -it $CONTAINER_NAME /bin/bash${NC}"
else
echo -e "${GREEN}✅ Container finished${NC}"
fi
else
echo -e "${RED}❌ Failed to start container${NC}"
exit 1
fi

View File

@ -1,68 +0,0 @@
#!/bin/bash
# Docker startup script for Hockey Results Application
set -e
echo "🏒 Starting Hockey Results Application..."
# Function to wait for database
wait_for_database() {
local db_type=$1
local max_attempts=30
local attempt=1
echo "⏳ Waiting for $db_type database to be ready..."
case $db_type in
"postgresql")
while [ $attempt -le $max_attempts ]; do
if python -c "import psycopg2; psycopg2.connect(host='$POSTGRES_HOST', port='$POSTGRES_PORT', database='$POSTGRES_DATABASE', user='$POSTGRES_USER', password='$POSTGRES_PASSWORD')" 2>/dev/null; then
echo "✅ PostgreSQL database is ready!"
return 0
fi
echo "Attempt $attempt/$max_attempts: Database not ready, waiting..."
sleep 2
attempt=$((attempt + 1))
done
;;
"mysql"|"mariadb")
while [ $attempt -le $max_attempts ]; do
if python -c "import pymysql; pymysql.connect(host='$MYSQL_HOST', port='$MYSQL_PORT', database='$MYSQL_DATABASE', user='$MYSQL_USER', password='$MYSQL_PASSWORD')" 2>/dev/null; then
echo "✅ MySQL/MariaDB database is ready!"
return 0
fi
echo "Attempt $attempt/$max_attempts: Database not ready, waiting..."
sleep 2
attempt=$((attempt + 1))
done
;;
esac
echo "❌ Database connection timeout after $max_attempts attempts"
exit 1
}
# Initialize database if needed
init_database() {
echo "🔧 Initializing database..."
python -c "
from motm_app.database import init_database
try:
init_database()
print('✅ Database initialized successfully')
except Exception as e:
print(f'⚠️ Database initialization warning: {e}')
"
}
# Wait for database if not using SQLite
if [ "$DATABASE_TYPE" != "sqlite" ]; then
wait_for_database "$DATABASE_TYPE"
fi
# Initialize database
init_database
# Start the application
echo "🚀 Starting Flask application..."
exec python motm_app/main.py

View File

@ -3,7 +3,8 @@ import pymysql
import os
from app import app
from flask_wtf import FlaskForm
from wtforms import BooleanField, StringField, PasswordField, IntegerField, TextAreaField, SubmitField, RadioField, SelectField, DateField
from wtforms import BooleanField, StringField, PasswordField, TextField, IntegerField, TextAreaField, SubmitField, RadioField, SelectField
from wtforms.fields.html5 import DateField
from wtforms_components import read_only
from wtforms import validators, ValidationError
from wtforms.validators import InputRequired, Email, Length
@ -26,24 +27,24 @@ class RegisterForm(FlaskForm):
class addPlayerForm(FlaskForm):
playerClub = SelectField('Club:', choices=[])
playerTeam = SelectField('Team:', choices=[])
playerForenames = StringField('Forenames:')
playerSurnames = StringField('Surname:')
playerNickname = StringField('Nickname')
playerChineseName = StringField('Chinese Name:')
playerEmailAddress = StringField('Email Address:')
playerForenames = TextField('Forenames:')
playerSurnames = TextField('Surname:')
playerNickname = TextField('Nickname')
playerChineseName = TextField('Chinese Name:')
playerEmailAddress = TextField('Email Address:')
playerDob = DateField('Date of Birth:', default=datetime.today, format='%Y-%m-%d')
playerHkid = StringField('HKID Number:')
playerNumber = StringField('Shirt Number:')
playerTelNumber = StringField('Player Contact Number:')
playerHkid = TextField('HKID Number:')
playerNumber = TextField('Shirt Number:')
playerTelNumber = TextField('Player Contact Number:')
submit = SubmitField('Submit')
class addTeamForm(FlaskForm):
clubName = SelectField("Club of team entry to create", coerce=str)
teamName = StringField("Team table to create (e.g. A, B, C, etc.)", validators=[InputRequired(), Length(max=1)])
teamName = TextField("Team table to create (e.g. A, B, C, etc.)", validators=[InputRequired(), Length(max=1)])
submit = SubmitField("Submit")
class addClubForm(FlaskForm):
clubName = StringField("Name of the Hockey Club to add")
clubName = TextField("Name of the Hockey Club to add")
submit = SubmitField("Submit")
class playerDbCreateForm(FlaskForm):
@ -77,6 +78,13 @@ class clubPlayingRecordsForm(FlaskForm):
clubName = SelectField("Club to search", choices=[], coerce=str)
submitButton = SubmitField("Submit")
class motmForm(FlaskForm):
startDate = DateField('DatePicker', format='%d-%m-%Y')
endDate = DateField('DatePicker', format='%d-%m-%Y')
class motmAdminForm(FlaskForm):
startDate = DateField('DatePicker', format='%d-%m-%Y')
endDate = DateField('DatePicker', format='%d-%m-%Y')
class squadListForm(FlaskForm):
teamName = SelectField("HKFC team to display")
@ -91,4 +99,28 @@ class adminSettingsForm(FlaskForm):
saveButton = SubmitField('Save Settings')
activateButton = SubmitField('Activate MotM Vote')
class goalsAssistsForm(FlaskForm):
fixtureNumber = TextField('Fixture Number')
match = SelectField('Fixture')
homeTeam = TextField('Home Team')
awayTeam = TextField('Away Team')
playerNumber = TextField('Player Number')
playerName = TextField('Player Name')
assists = SelectField('Assists:', choices=[(0, '0'), (1, '1'), (2, '2'), (3, '3'), (4, '4')])
goals = SelectField('Goals:', choices=[(0, '0'), (1, '1'), (2, '2'), (3, '3'), (4, '4')])
submit = SubmitField('Submit')
# def __init__(self, *args, **kwargs):
# super(goalsAssistsForm, self).__init__(*args, **kwargs)
# read_only(self.homeTeam)
# read_only(self.awayTeam)
class adminSettingsForm2(FlaskForm):
nextMatch = SelectField('Fixture', choices=[], default=mySettings('match'))
nextOppoClub = TextField('Next Opposition Club:', default=mySettings('club'))
nextOppoTeam = TextField("Next Opposition Team:")
currMotM = SelectField('Current Man of the Match:', choices=[], default=mySettings('motm'))
currDotD = SelectField('Current Dick of the Day:', choices=[], default=mySettings('dotd'))
saveButton = SubmitField('Save Settings')
activateButton = SubmitField('Activate MotM Vote')

17
main.py
View File

@ -11,7 +11,7 @@ from flask import Flask, flash, render_template, request, redirect, url_for
from flask_wtf import FlaskForm
from flask_bootstrap import Bootstrap
from wtforms import StringField, PasswordField, BooleanField
from wtforms import DateField
from wtforms.fields.html5 import DateField
from wtforms.validators import InputRequired, Email, Length
from forms import LoginForm, RegisterForm
from dbWrite import sql_write, sql_write_static, sql_read, sql_read_static
@ -20,6 +20,19 @@ from routes import *
app.register_blueprint(routes)
@app.route('/hkfc-d/vote-chart', methods=['GET', 'POST'])
def hkfc_d_vote_chart():
form = LoginForm()
print('Here we are')
if form.validate_on_submit():
sql = "SELECT username FROM hockeyUsers WHERE (username= '" + form.username.data + "')"
print(sql)
rows = sql_read(sql)
print(rows)
return redirect(url_for('/hkfc-d/voting'))
# return '<h1>Something went wrong there</h1>'
return render_template('hkfc-d/login-vote.html', form=form)
@app.route('/login', methods=['GET', 'POST'])
def login():
@ -31,7 +44,7 @@ def login():
rows = sql_write(sql)
print(rows)
print(rows[0])
return redirect(url_for('dashboard'))
return redirect(url_for('/hkfc-d/voting'))
else:
return 'Something went wrong'
# return '<h1>Something went wrong there</h1>'

View File

@ -1,80 +0,0 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
venv/
env/
ENV/
env.bak/
venv.bak/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Database files
*.db
*.sqlite
*.sqlite3
# Logs
*.log
logs/
# Temporary files
*.tmp
*.temp
# Git
.git/
.gitignore
# Documentation
*.md
docs/
# Scripts and configs not needed in container
activate_motm.bat
activate_motm.sh
run_motm.bat
run_motm.sh
setup_venv_windows.bat
setup_venv.ps1
setup_venv.py
deploy.py
test_app.py
# Development files
app.yaml
database_config.ini
POSTGRESQL_SETUP.md
SETUP_GUIDE.md
VIRTUAL_ENV_GUIDE.md
README.md

150
motm_app/.gitignore vendored
View File

@ -1,150 +0,0 @@
# Virtual Environment
venv/
env/
ENV/
.venv/
.env/
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
Pipfile.lock
# PEP 582
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Application specific
*.db
*.sqlite
config.py
.env.local
.env.production

View File

@ -1,190 +0,0 @@
# MOTM Application - Container Deployment
This document provides instructions for deploying the MOTM (Man of the Match) application using Docker containers.
## Quick Start
### Using Docker Compose (Recommended)
1. **Clone the repository and navigate to the project directory**
```bash
cd motm_app
```
2. **Start the application with PostgreSQL**
```bash
docker-compose up -d
```
3. **Access the application**
- Main page: http://localhost:5000
- Admin dashboard: http://localhost:5000/admin (username: `admin`, password: `letmein`)
### Using Docker Build
1. **Build the container**
```bash
docker build -f Containerfile -t motm-app .
```
2. **Run with external PostgreSQL**
```bash
docker run -d \
--name motm-app \
-p 5000:5000 \
-e DATABASE_TYPE=postgresql \
-e DB_HOST=your-postgres-host \
-e DB_PORT=5432 \
-e DB_NAME=motm_db \
-e DB_USER=motm_user \
-e DB_PASSWORD=motm_password \
motm-app
```
## Configuration
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `DATABASE_TYPE` | `postgresql` | Database type (postgresql/sqlite) |
| `DB_HOST` | `postgres` | Database host (use `postgres` for docker-compose) |
| `DB_PORT` | `5432` | Database port |
| `DB_NAME` | `motm_db` | Database name |
| `DB_USER` | `motm_user` | Database username |
| `DB_PASSWORD` | `motm_password` | Database password |
| `FLASK_ENV` | `production` | Flask environment |
| `FLASK_RUN_HOST` | `0.0.0.0` | Flask host |
| `FLASK_RUN_PORT` | `5000` | Flask port |
| `SECRET_KEY` | `your-secret-key-change-this-in-production` | Flask secret key |
### Production Security
**Important**: Before deploying to production, change the following:
1. **Database credentials** in `docker-compose.yml`
2. **Secret key** in environment variables
3. **Admin password** (use the admin profile page after first login)
## Container Features
### Multi-stage Build
- **Builder stage**: Installs dependencies and builds Python packages
- **Runtime stage**: Minimal image with only runtime dependencies
### Security
- Runs as non-root user (`appuser`)
- Minimal attack surface with slim base image
- Health checks for container monitoring
### Database Integration
- Automatic database connection waiting
- Database initialization support
- PostgreSQL optimized configuration
## Monitoring
### Health Checks
- Application health check: `curl -f http://localhost:5000/`
- Database health check: `pg_isready`
### Logs
```bash
# View application logs
docker-compose logs motm-app
# View database logs
docker-compose logs postgres
# Follow logs in real-time
docker-compose logs -f motm-app
```
## Maintenance
### Backup Database
```bash
# Create backup
docker exec motm-postgres pg_dump -U motm_user motm_db > backup.sql
# Restore backup
docker exec -i motm-postgres psql -U motm_user motm_db < backup.sql
```
### Update Application
```bash
# Pull latest changes
git pull
# Rebuild and restart
docker-compose down
docker-compose up -d --build
```
### Reset Database
```bash
# Stop services
docker-compose down
# Remove database volume
docker volume rm motm_app_postgres_data
# Start fresh
docker-compose up -d
```
## Troubleshooting
### Common Issues
1. **Database connection errors**
- Check if PostgreSQL container is running: `docker-compose ps`
- Verify database credentials in environment variables
- Check database logs: `docker-compose logs postgres`
2. **Application won't start**
- Check application logs: `docker-compose logs motm-app`
- Verify all environment variables are set correctly
- Ensure database is healthy before application starts
3. **Port conflicts**
- Change port mapping in `docker-compose.yml`
- Example: `"8080:5000"` to use port 8080 instead of 5000
### Debug Mode
```bash
# Run container in interactive mode for debugging
docker run -it --rm \
-p 5000:5000 \
-e DATABASE_TYPE=postgresql \
-e DB_HOST=your-postgres-host \
-e DB_PORT=5432 \
-e DB_NAME=motm_db \
-e DB_USER=motm_user \
-e DB_PASSWORD=motm_password \
motm-app /bin/bash
```
## File Structure
```
motm_app/
├── Containerfile # Docker container definition
├── docker-compose.yml # Multi-service orchestration
├── .dockerignore # Files to exclude from build
├── init.sql # Database initialization script
├── requirements.txt # Python dependencies
├── main.py # Main application file
├── static/ # Static assets (CSS, JS, images)
├── templates/ # HTML templates
└── data/ # Persistent data directory
```
## Support
For issues or questions about container deployment, please check:
1. Application logs: `docker-compose logs motm-app`
2. Database logs: `docker-compose logs postgres`
3. Container status: `docker-compose ps`

View File

@ -1,95 +0,0 @@
# Containerfile for MOTM (Man of the Match) Application
# Flask-based voting system for hockey matches
# Stage 1: Build stage
FROM python:3.11-slim as builder
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Install system dependencies for building Python packages
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
default-libmysqlclient-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
# Create and activate virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Copy requirements and install Python dependencies
COPY requirements.txt ./
RUN pip install --upgrade pip && \
pip install -r requirements.txt
# Stage 2: Runtime stage
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH" \
DATABASE_TYPE=postgresql \
FLASK_ENV=production \
FLASK_APP=main.py \
FLASK_RUN_HOST=0.0.0.0 \
FLASK_RUN_PORT=5000
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
libpq5 \
default-mysql-client \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy virtual environment from builder stage
COPY --from=builder /opt/venv /opt/venv
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Create application directory
WORKDIR /app
# Copy application code
COPY --chown=appuser:appuser . .
# Create directories for data and logs
RUN mkdir -p /app/data /app/logs && \
chown -R appuser:appuser /app/data /app/logs
# Create a startup script for better initialization
RUN echo '#!/bin/bash\n\
# Wait for database to be ready\n\
echo "Waiting for database connection..."\n\
while ! python -c "import psycopg2; psycopg2.connect(host=\"$DB_HOST\", port=\"$DB_PORT\", user=\"$DB_USER\", password=\"$DB_PASSWORD\", dbname=\"$DB_NAME\")" 2>/dev/null; do\n\
echo "Database not ready, waiting..."\n\
sleep 2\n\
done\n\
echo "Database connection established!"\n\
\n\
# Initialize database if needed\n\
python -c "from db_setup import db_config_manager; db_config_manager.load_config(); db_config_manager._update_environment_variables()"\n\
\n\
# Start the application\n\
exec python main.py' > /app/start.sh && \
chmod +x /app/start.sh && \
chown appuser:appuser /app/start.sh
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 5000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:5000/ || exit 1
# Default command
CMD ["/app/start.sh"]

View File

@ -1,60 +0,0 @@
# PostgreSQL Database Setup
This application is now configured to use PostgreSQL as the default database instead of SQLite.
## Database Configuration
The application connects to a PostgreSQL database with the following settings:
- **Host**: icarus.ipa.champion
- **Port**: 5432
- **Database**: motm
- **Username**: motm_user
- **Password**: q7y7f7Lv*sODJZ2wGiv0Wq5a
## Running the Application
### Linux/macOS
```bash
./run_motm.sh
```
### Windows
```cmd
run_motm.bat
```
### Manual Setup
If you need to run the application manually, set these environment variables:
```bash
export DATABASE_TYPE=postgresql
export POSTGRES_HOST=icarus.ipa.champion
export POSTGRES_PORT=5432
export POSTGRES_DATABASE=motm
export POSTGRES_USER=motm_user
export POSTGRES_PASSWORD='q7y7f7Lv*sODJZ2wGiv0Wq5a'
```
## Database Status
The PostgreSQL database contains the following data:
- **Players**: 3 players in the `_hkfc_players` table
- **Match Squad**: 3 players currently selected for the match squad
- **Admin Settings**: Configured with next match details
## Troubleshooting
If you encounter issues:
1. **Connection Failed**: Check if the PostgreSQL server is accessible
2. **Empty Data**: The database contains sample data - if you see empty tables, check the connection
3. **Permission Errors**: Ensure the `motm_user` has proper database permissions
## Testing Database Connection
Run the test script to verify everything is working:
```bash
python3 test_match_squad.py
```
This will test the database connection and display current data.

View File

@ -1,226 +0,0 @@
# HKFC Men's C Team - MOTM (Man of the Match) System
This is a standalone Flask application for managing Man of the Match and Dick of the Day voting for the HKFC Men's C Team hockey club.
## Features
### Public Section
- **Voting Interface**: Players can vote for MOTM and DotD via secure random URLs
- **Match Comments**: View and add comments from matches
- **Current Holders**: Display current MOTM and DotD holders
### Admin Section
- **Match Management**: Set up upcoming matches, opposition teams, and fixtures
- **Squad Management**: Add/remove players from match squads
- **Statistics**: Record goals and assists for players
- **Voting Results**: View real-time voting charts and results
- **Player of the Year**: Track season-long MOTM/DotD statistics
## Installation
### Option 1: Using Virtual Environment (Recommended)
**Windows Setup:**
1. **Create virtual environment**:
```cmd
python -m venv venv
```
*If `python` doesn't work, try `py` or use the full path to python.exe*
2. **Activate virtual environment**:
```cmd
venv\Scripts\activate.bat
```
3. **Install dependencies**:
```cmd
pip install --upgrade pip
pip install -r requirements.txt
```
4. **Run the application**:
```cmd
python main.py
```
**Unix/Linux/Mac Setup:**
1. **Create virtual environment**:
```bash
python3 -m venv venv
```
2. **Activate virtual environment**:
```bash
source venv/bin/activate
```
3. **Install dependencies**:
```bash
pip install --upgrade pip
pip install -r requirements.txt
```
4. **Run the application**:
```bash
python main.py
```
**Convenience Scripts:**
- After setup, you can use `activate_motm.bat` (Windows) or `source activate_motm.sh` (Unix)
- Or use `run_motm.bat` (Windows) or `./run_motm.sh` (Unix) to start directly
### Option 2: Manual Virtual Environment Setup
1. **Create virtual environment**:
```bash
python -m venv venv
```
2. **Activate virtual environment**:
- **Windows**: `venv\Scripts\activate`
- **Unix/Linux/Mac**: `source venv/bin/activate`
3. **Install dependencies**:
```bash
pip install -r requirements.txt
```
4. **Configure database settings** in `db_config.py`
5. **Run the application**:
```bash
python main.py
```
### Option 3: Global Installation (Not Recommended)
1. Install dependencies:
```bash
pip install -r requirements.txt
```
2. Configure database settings in `db_config.py`
3. Run the application:
```bash
python main.py
```
The application will be available at `http://localhost:5000`
## Database Requirements
The application requires access to the following database tables:
- `_hkfcC_matchSquad` - Current match squad
- `_HKFC_players` - Player database
- `hkfcCAdminSettings` - Admin configuration
- `hockeyFixtures` - Match fixtures
- `_hkfc_c_motm` - MOTM/DotD voting results
- `_motmComments` - Match comments
- `_clubTeams` - Club and team information
- `mensHockeyClubs` - Club logos and information
## API Endpoints
### Public Endpoints
- `/` - Main index page
- `/motm/<randomUrlSuffix>` - Voting page (requires valid URL suffix)
- `/motm/comments` - Match comments
- `/motm/vote-thanks` - Vote submission processing
### Admin Endpoints (Basic Auth Required)
- `/admin/motm` - MOTM administration
- `/admin/squad` - Squad management
- `/admin/squad/submit` - Process squad selection
- `/admin/squad/list` - View current squad
- `/admin/squad/remove` - Remove player from squad
- `/admin/squad/reset` - Reset squad for new match
- `/admin/stats` - Goals and assists administration
- `/admin/voting` - Voting results charts
- `/admin/poty` - Player of the Year charts
### API Endpoints
- `/api/vote-results` - Get voting results as JSON
- `/api/poty-results` - Get Player of the Year results as JSON
- `/admin/api/team/<club>` - Get teams for a club
- `/admin/api/logo/<club>` - Get club logo URL
- `/admin/api/fixture/<fixture>` - Get fixture information
## Security
- Admin sections are protected with HTTP Basic Authentication
- Voting URLs use random suffixes to prevent unauthorized access
- All admin actions require authentication
## Usage
1. **Admin Setup**:
- Access `/admin/motm` to configure upcoming matches
- Use `/admin/squad` to select players for the match
- Activate voting to generate the public voting URL
2. **Player Voting**:
- Share the generated voting URL with players
- Players can vote for MOTM and DotD
- Optional comments can be added
3. **Results**:
- View real-time results at `/admin/voting`
- Track season statistics at `/admin/poty`
## Configuration
Update the following in `db_config.py`:
- Database connection details
- Cloud SQL configuration (if using Google Cloud)
- Local database settings
The application uses the same database as the main hockey results system, so ensure proper database access is configured.
## Virtual Environment Management
### Development Workflow
1. **Activate virtual environment** before making changes:
- Windows: `activate_motm.bat` or `venv\Scripts\activate`
- Unix/Linux/Mac: `source activate_motm.sh` or `source venv/bin/activate`
2. **Install new packages**:
```bash
pip install new-package-name
```
3. **Update requirements.txt** after adding packages:
```bash
pip freeze > requirements.txt
```
4. **Deactivate** when done:
```bash
deactivate
```
### Virtual Environment Benefits
- **Isolation**: Dependencies won't conflict with other Python projects
- **Version Control**: Pin specific package versions for reproducibility
- **Clean Environment**: Easy to recreate if corrupted
- **Deployment**: Same environment can be replicated in production
### Troubleshooting
**Virtual environment not activating?**
- Ensure Python 3.7+ is installed
- Check file permissions on activation scripts
- Try recreating: `python setup_venv.py`
**Dependencies not found?**
- Activate virtual environment first
- Check if packages are installed: `pip list`
- Reinstall requirements: `pip install -r requirements.txt`
**Permission errors on Unix/Linux/Mac?**
- Make scripts executable: `chmod +x *.sh`
- Run with proper permissions

View File

@ -1,213 +0,0 @@
# MOTM Application - Virtual Environment Setup Guide
This guide will help you set up a Python virtual environment for the MOTM Flask application.
## Prerequisites
1. **Python 3.7 or higher** must be installed on your system
2. **pip** package manager should be available
## Quick Setup (Windows)
### Step 1: Check Python Installation
Open Command Prompt or PowerShell and run:
```cmd
python --version
```
If Python is not found, you may need to:
- Install Python from [python.org](https://www.python.org/downloads/)
- Or use `py` command if you have Python Launcher installed
### Step 2: Create Virtual Environment
Navigate to the `motm_app` directory and run:
**Option A: Using `python` command**
```cmd
python -m venv venv
```
**Option B: Using `py` command (if available)**
```cmd
py -m venv venv
```
**Option C: Using full path (if needed)**
```cmd
C:\Python39\python.exe -m venv venv
```
### Step 3: Activate Virtual Environment
**Windows Command Prompt:**
```cmd
venv\Scripts\activate.bat
```
**Windows PowerShell:**
```powershell
venv\Scripts\Activate.ps1
```
### Step 4: Install Dependencies
With the virtual environment activated:
```cmd
pip install --upgrade pip
pip install -r requirements.txt
```
### Step 5: Run the Application
```cmd
python main.py
```
The application will be available at: http://localhost:5000
## Quick Setup (Unix/Linux/Mac)
### Step 1: Create Virtual Environment
```bash
python3 -m venv venv
```
### Step 2: Activate Virtual Environment
```bash
source venv/bin/activate
```
### Step 3: Install Dependencies
```bash
pip install --upgrade pip
pip install -r requirements.txt
```
### Step 4: Run the Application
```bash
python main.py
```
## Using the Convenience Scripts
After setting up the virtual environment, you can use the provided scripts:
### Windows
- **Setup**: `setup_venv_windows.bat`
- **Activate**: `activate_motm.bat`
- **Run**: `run_motm.bat`
### Unix/Linux/Mac
- **Activate**: `source activate_motm.sh`
- **Run**: `./run_motm.sh`
## Troubleshooting
### Python Not Found
**Windows:**
1. Install Python from [python.org](https://www.python.org/downloads/)
2. During installation, check "Add Python to PATH"
3. Restart your command prompt
**Alternative for Windows:**
1. Install Python from Microsoft Store
2. Use `py` command instead of `python`
### Virtual Environment Issues
**Permission Errors (Windows):**
```cmd
# Try running as Administrator
# Or use PowerShell with execution policy
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
**Virtual Environment Already Exists:**
```cmd
# Remove existing environment
rmdir /s venv
# Or on Unix/Linux/Mac
rm -rf venv
```
### Dependencies Not Installing
1. **Activate virtual environment first**
2. **Upgrade pip**: `pip install --upgrade pip`
3. **Install individually**: `pip install Flask`
4. **Check internet connection**
5. **Try using --user flag**: `pip install --user -r requirements.txt`
### Application Not Starting
1. **Check virtual environment is activated** (should see `(venv)` in prompt)
2. **Verify all dependencies installed**: `pip list`
3. **Check for import errors**: `python -c "import flask"`
4. **Review error messages** in console output
## Development Workflow
### Daily Usage
1. **Activate environment**: `venv\Scripts\activate.bat` (Windows) or `source venv/bin/activate` (Unix)
2. **Make changes** to your code
3. **Test application**: `python main.py`
4. **Deactivate when done**: `deactivate`
### Adding New Packages
1. **Activate environment**
2. **Install package**: `pip install new-package`
3. **Update requirements**: `pip freeze > requirements.txt`
4. **Commit changes** to version control
### Updating Dependencies
1. **Activate environment**
2. **Update packages**: `pip install --upgrade package-name`
3. **Update requirements**: `pip freeze > requirements.txt`
## Environment Variables
If you need to set environment variables for the application:
**Windows:**
```cmd
set DATABASE_URL=your_database_url
python main.py
```
**Unix/Linux/Mac:**
```bash
export DATABASE_URL=your_database_url
python main.py
```
## Database Configuration
Make sure to update `db_config.py` with your database connection details before running the application.
## Getting Help
If you encounter issues:
1. **Check Python version**: Should be 3.7 or higher
2. **Verify virtual environment**: Should be activated
3. **Check dependencies**: Run `pip list` to see installed packages
4. **Review error messages**: Look for specific error details
5. **Test imports**: `python -c "import flask, pymysql"`
## Production Deployment
For production deployment:
1. **Use same Python version** as development
2. **Install exact dependencies**: `pip install -r requirements.txt`
3. **Set environment variables** for production database
4. **Configure web server** (nginx, Apache, etc.)
5. **Use process manager** (systemd, supervisor, etc.)
---
**Note**: This application requires access to the existing hockey results database. Ensure proper database connectivity before running.

View File

@ -1,254 +0,0 @@
# MOTM Application - Virtual Environment Guide
## 🎯 Why Use Virtual Environments?
Virtual environments provide **isolated Python environments** for your projects, which means:
- ✅ **No conflicts** between different project dependencies
- ✅ **Reproducible environments** across different machines
- ✅ **Clean separation** of project requirements
- ✅ **Easy deployment** with exact dependency versions
## 🚀 Quick Start
### Windows Users
1. **Open Command Prompt** (not PowerShell initially)
2. **Navigate to motm_app directory**:
```cmd
cd path\to\motm_app
```
3. **Create virtual environment**:
```cmd
python -m venv venv
```
*If this fails, try `py -m venv venv` or find your Python installation*
4. **Activate virtual environment**:
```cmd
venv\Scripts\activate.bat
```
*You should see `(venv)` at the beginning of your command prompt*
5. **Install dependencies**:
```cmd
pip install --upgrade pip
pip install -r requirements.txt
```
6. **Run the application**:
```cmd
python main.py
```
### Mac/Linux Users
1. **Open Terminal**
2. **Navigate to motm_app directory**:
```bash
cd path/to/motm_app
```
3. **Create virtual environment**:
```bash
python3 -m venv venv
```
4. **Activate virtual environment**:
```bash
source venv/bin/activate
```
*You should see `(venv)` at the beginning of your command prompt*
5. **Install dependencies**:
```bash
pip install --upgrade pip
pip install -r requirements.txt
```
6. **Run the application**:
```bash
python main.py
```
## 📁 What Gets Created
When you create a virtual environment, you'll see a new `venv` folder:
```
motm_app/
├── venv/ # Virtual environment (don't edit this)
│ ├── Scripts/ # Windows activation scripts
│ ├── bin/ # Unix activation scripts
│ ├── Lib/ # Installed packages
│ └── pyvenv.cfg # Environment configuration
├── main.py # Your application
├── requirements.txt # Dependencies list
└── ... # Other files
```
## 🔧 Daily Usage
### Starting Your Work Session
1. **Activate the virtual environment**:
- Windows: `venv\Scripts\activate.bat`
- Mac/Linux: `source venv/bin/activate`
2. **Verify activation** - you should see `(venv)` in your prompt:
```cmd
(venv) C:\path\to\motm_app>
```
3. **Run your application**:
```cmd
python main.py
```
### Ending Your Work Session
**Deactivate when done**:
```cmd
deactivate
```
*The `(venv)` indicator will disappear*
## 📦 Managing Dependencies
### Installing New Packages
1. **Activate virtual environment first**
2. **Install the package**:
```cmd
pip install package-name
```
3. **Update requirements.txt**:
```cmd
pip freeze > requirements.txt
```
4. **Commit the updated requirements.txt** to version control
### Updating Existing Packages
```cmd
pip install --upgrade package-name
```
### Viewing Installed Packages
```cmd
pip list
```
### Removing Packages
```cmd
pip uninstall package-name
```
## 🛠️ Troubleshooting
### "Python not found" Error
**Windows:**
- Install Python from [python.org](https://www.python.org/downloads/)
- During installation, check "Add Python to PATH"
- Try using `py` command instead of `python`
**Mac/Linux:**
- Install Python 3: `brew install python3` (Mac) or use package manager
- Use `python3` instead of `python`
### Virtual Environment Won't Activate
**Windows:**
```cmd
# Try running Command Prompt as Administrator
# Or use PowerShell with:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
**Permission Errors:**
```cmd
# Remove and recreate
rmdir /s venv # Windows
rm -rf venv # Mac/Linux
python -m venv venv # Recreate
```
### Packages Not Found After Installation
1. **Make sure virtual environment is activated** (look for `(venv)`)
2. **Check if packages are installed**: `pip list`
3. **Reinstall requirements**: `pip install -r requirements.txt`
### Application Won't Start
1. **Activate virtual environment**
2. **Check all dependencies**: `pip install -r requirements.txt`
3. **Test imports**: `python -c "import flask"`
4. **Check error messages** in the console
## 🔄 Working with Teams
### Sharing Your Environment
1. **Commit `requirements.txt`** to version control
2. **Don't commit the `venv` folder** (add to `.gitignore`)
### Setting Up on New Machine
1. **Clone the repository**
2. **Create virtual environment**: `python -m venv venv`
3. **Activate it**: `venv\Scripts\activate.bat` (Windows) or `source venv/bin/activate` (Unix)
4. **Install dependencies**: `pip install -r requirements.txt`
## 🚀 Deployment Considerations
### Production Environment
- Use the same Python version as development
- Install exact dependencies: `pip install -r requirements.txt`
- Set up environment variables for database connections
- Use a proper web server (nginx, Apache)
- Consider using Docker for containerization
### Docker Example
```dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "main.py"]
```
## 💡 Best Practices
1. **Always use virtual environments** for Python projects
2. **Activate before working** on the project
3. **Keep requirements.txt updated** when adding packages
4. **Use descriptive commit messages** when updating dependencies
5. **Test your application** after installing new packages
6. **Document any special setup requirements**
## 🆘 Getting Help
If you're still having issues:
1. **Check Python version**: Should be 3.7 or higher
2. **Verify virtual environment**: Should be activated (see `(venv)` in prompt)
3. **Review error messages**: Look for specific error details
4. **Test step by step**: Create a simple test script to verify Python works
5. **Check internet connection**: Required for installing packages
---
**Remember**: The virtual environment isolates your project dependencies from your system Python installation, making your project more reliable and portable! 🎉

View File

@ -1,10 +0,0 @@
@echo off
echo 🐍 Activating MOTM Virtual Environment...
call venv\Scripts\activate.bat
echo ✅ Virtual environment activated!
echo.
echo 🚀 To start the MOTM application, run:
echo python.exe main.py
echo.
echo 🔧 To deactivate, run:
echo deactivate

View File

@ -1,11 +0,0 @@
#!/bin/bash
echo "🐍 Activating MOTM Virtual Environment..."
source venv/bin/activate
echo "✅ Virtual environment activated!"
echo ""
echo "🚀 To start the MOTM application, run:"
echo " python main.py"
echo ""
echo "🔧 To deactivate, run:"
echo " deactivate"

View File

@ -1,112 +0,0 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version number format
version_num_format = %04d
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses
# os.pathsep. If this key is omitted entirely, it falls back to the legacy
# behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = sqlite:///hockey_results.db
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -1,13 +0,0 @@
# encoding=utf-8
import random
import string
from flask import Flask
from flask_bootstrap import Bootstrap
app = Flask(__name__)
app.secret_key = "4pFwRNNXs+xQSOEaHrq4iSBwl+mq1UTdRuxqhM+RQpo="
Bootstrap(app)
def randomUrlSuffix(stringLength=6):
lettersAndDigits = string.ascii_letters + string.digits
return ''.join(random.choice(lettersAndDigits) for i in range(stringLength))

View File

@ -1,24 +0,0 @@
runtime: python39
env_variables:
CLOUDSQL_CONNECTION_NAME: "hk-hockey:asia-east2:hk-hockey-sql"
CLOUDSQL_USER: "hockeyWrite"
CLOUDSQL_WRITE_USER: "hockeyWrite"
CLOUDSQL_READ_USER: "hockeyRead"
CLOUDSQL_PASSWORD: "P8P1YopMlwg8TxhE"
CLOUDSQL_WRITE_PASSWORD: "1URYcxXXlQ6xOWgj"
CLOUDSQL_READ_PASSWORD: "o4GWrbbkBKy3oR6u"
CLOUDSQL_DATABASE: "20209_hockeyResults"
CLOUDSQL_DATABASE_STATIC: "hockeyResults"
CLOUDSQL_CHARSET: "utf8"
handlers:
- url: /static
static_dir: static
- url: /.*
script: auto
automatic_scaling:
min_instances: 1
max_instances: 10

View File

@ -1,237 +0,0 @@
# encoding=utf-8
"""
Club scraper for Hong Kong Hockey Association website
Fetches men's hockey clubs from https://hockey.org.hk
"""
import requests
from bs4 import BeautifulSoup
import re
class ClubScraper:
"""Scrapes club data from Hong Kong Hockey Association website"""
CLUBS_URL = "https://hockey.org.hk/Content.asp?Uid=27"
# Common club abbreviations and their full names
CLUB_ABBREVIATIONS = {
'Pak': 'Pakistan Association of HK Ltd.',
'KCC': 'Kowloon Cricket Club',
'HKFC': 'Hong Kong Football Club',
'USRC': 'United Services Recreation Club',
'Valley': 'Valley Fort Sports Club',
'SSSC': 'South China Sports Club',
'Dragons': 'Dragons Hockey Club',
'Kai Tak': 'Kai Tak Sports Club',
'RHOBA': 'Royal Hong Kong Regiment Officers and Businessmen Association',
'Elite': 'Elite Hockey Club',
'Aquila': 'Aquila Hockey Club',
'HKJ': 'Hong Kong Jockey Club',
'Sirius': 'Sirius Hockey Club',
'Shaheen': 'Shaheen Hockey Club',
'Diocesan': 'Diocesan Boys School',
'Rhino': 'Rhino Hockey Club',
'Khalsa': 'Khalsa Hockey Club',
'HKCC': 'Hong Kong Cricket Club',
'Police': 'Hong Kong Police Force',
'Recreio': 'Recreio Hockey Club',
'CSD': 'Correctional Services Department',
'Dutch': 'Dutch Hockey Club',
'HKUHC': 'Hong Kong University Hockey Club',
'Kaitiaki': 'Kaitiaki Hockey Club',
'Antlers': 'Antlers Hockey Club',
'Marcellin': 'Marcellin Hockey Club',
'Skyers': 'Skyers Hockey Club',
'JR': 'JR Hockey Club',
'IUHK': 'International University of Hong Kong',
'144U': '144 United Hockey Club',
'HKU': 'Hong Kong University',
'UBSC': 'United Brother Sports Club',
'Nanki': 'Nanki Sports Club',
'Gojra': 'Gojra Hockey Club',
'KNS': 'KNS Hockey Club',
'Hockey Clube de Macau': 'Hockey Clube de Macau'
}
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
})
def fetch_clubs(self):
"""Fetch and parse clubs from the website"""
try:
response = self.session.get(self.CLUBS_URL, timeout=10)
response.raise_for_status()
return self._parse_clubs(response.text)
except requests.RequestException as e:
print(f"Error fetching clubs: {e}")
return []
def _parse_clubs(self, html_content):
"""Parse HTML content and extract club information"""
soup = BeautifulSoup(html_content, 'lxml')
clubs = []
# Look for tables or structured data containing club information
tables = soup.find_all('table')
for table in tables:
rows = table.find_all('tr')
for row in rows:
cells = row.find_all(['td', 'th'])
if len(cells) >= 2:
# Extract club name from first cell
club_name = cells[0].get_text(strip=True)
# Skip header rows and empty cells
if not club_name or club_name.lower() in ['club', 'name', 'abbreviation', 'team', 'clubs']:
continue
# Skip if it's clearly a header row
if club_name == 'Clubs' and abbreviation == 'Abbreviated Title':
continue
# Extract abbreviation if available
abbreviation = None
if len(cells) > 1:
abbreviation = cells[1].get_text(strip=True)
# Extract teams if available
teams = []
if len(cells) > 2:
teams_text = cells[2].get_text(strip=True)
# Parse teams (e.g., "A, B" or "A B")
if teams_text:
teams = [team.strip() for team in re.split(r'[,;]', teams_text) if team.strip()]
# Extract convenor if available
convenor = None
if len(cells) > 3:
convenor = cells[3].get_text(strip=True)
# Extract email if available
email = None
if len(cells) > 4:
email = cells[4].get_text(strip=True)
club_data = {
'name': club_name,
'abbreviation': abbreviation,
'teams': teams,
'convenor': convenor,
'email': email
}
clubs.append(club_data)
# If no structured data found, try to extract from text content
if not clubs:
clubs = self._extract_clubs_from_text(html_content)
return clubs
def _extract_clubs_from_text(self, html_content):
"""Extract club names from text content if no structured data found"""
soup = BeautifulSoup(html_content, 'lxml')
clubs = []
# Look for common patterns in text
text_content = soup.get_text()
# Extract known club names from the text
for abbreviation, full_name in self.CLUB_ABBREVIATIONS.items():
if abbreviation in text_content or full_name in text_content:
clubs.append({
'name': full_name,
'abbreviation': abbreviation,
'teams': [],
'convenor': None,
'email': None
})
return clubs
def get_clubs_with_abbreviations(self):
"""Get clubs with proper abbreviation handling"""
clubs = self.fetch_clubs()
# Process clubs to handle abbreviations
processed_clubs = []
for club in clubs:
name = club['name']
abbreviation = club.get('abbreviation', '')
# If we have an abbreviation, check if it's in our mapping
if abbreviation and abbreviation in self.CLUB_ABBREVIATIONS:
full_name = self.CLUB_ABBREVIATIONS[abbreviation]
processed_club = club.copy()
processed_club['name'] = full_name
processed_club['abbreviation'] = abbreviation
processed_clubs.append(processed_club)
elif name in self.CLUB_ABBREVIATIONS.values():
# If the name is already a full name, find its abbreviation
for abbr, full in self.CLUB_ABBREVIATIONS.items():
if full == name:
processed_club = club.copy()
processed_club['abbreviation'] = abbr
processed_clubs.append(processed_club)
break
else:
# Keep as-is if no mapping found
processed_clubs.append(club)
return processed_clubs
def get_club_logo_url(self, club_name):
"""Generate a logo URL for a club (placeholder implementation)"""
# This could be enhanced to fetch actual logos from the website
# For now, return a placeholder
club_slug = club_name.lower().replace(' ', '_').replace('.', '').replace(',', '')
return f"/static/images/clubs/{club_slug}_logo.png"
def get_hk_hockey_clubs():
"""Convenience function to get Hong Kong hockey clubs"""
scraper = ClubScraper()
return scraper.get_clubs_with_abbreviations()
def expand_club_abbreviation(abbreviation):
"""Expand a club abbreviation to its full name"""
return ClubScraper.CLUB_ABBREVIATIONS.get(abbreviation, abbreviation)
if __name__ == "__main__":
# Test the scraper
print("Testing Hong Kong Hockey Club Scraper...")
print("=" * 60)
scraper = ClubScraper()
print("\nFetching clubs from Hockey Hong Kong website...")
clubs = scraper.get_clubs_with_abbreviations()
if clubs:
print(f"\nFound {len(clubs)} clubs:")
for i, club in enumerate(clubs, 1):
print(f"\n{i}. {club['name']}")
if club.get('abbreviation'):
print(f" Abbreviation: {club['abbreviation']}")
if club.get('teams'):
print(f" Teams: {', '.join(club['teams'])}")
if club.get('convenor'):
print(f" Convenor: {club['convenor']}")
if club.get('email'):
print(f" Email: {club['email']}")
else:
print("\nNo clubs found. This might be due to website structure changes.")
print("Using fallback club list...")
# Fallback to known clubs
for abbreviation, full_name in scraper.CLUB_ABBREVIATIONS.items():
print(f"- {full_name} ({abbreviation})")
print("\n" + "=" * 60)

View File

@ -1,294 +0,0 @@
# encoding=utf-8
"""
Database configuration and models for multi-database support using SQLAlchemy.
Supports PostgreSQL, MariaDB/MySQL, and SQLite.
"""
import os
from sqlalchemy import create_engine, Column, Integer, String, Date, DateTime, Text, SmallInteger, ForeignKey, Boolean, Float
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import JSON
from datetime import datetime
# Base class for all models
Base = declarative_base()
class DatabaseConfig:
"""Database configuration class for multiple database support."""
def __init__(self):
self.database_url = self._get_database_url()
self.engine = create_engine(self.database_url, echo=False)
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
def _get_database_url(self):
"""Get database URL from environment variables or configuration."""
db_type = os.getenv('DATABASE_TYPE', 'sqlite').lower()
if db_type == 'postgresql':
return self._get_postgresql_url()
elif db_type in ['mysql', 'mariadb']:
return self._get_mysql_url()
elif db_type == 'sqlite':
return self._get_sqlite_url()
else:
raise ValueError(f"Unsupported database type: {db_type}")
def _get_postgresql_url(self):
"""Get PostgreSQL connection URL."""
host = os.getenv('POSTGRES_HOST', 'localhost')
port = os.getenv('POSTGRES_PORT', '5432')
database = os.getenv('POSTGRES_DATABASE', 'hockey_results')
username = os.getenv('POSTGRES_USER', 'postgres')
password = os.getenv('POSTGRES_PASSWORD', '')
return f"postgresql://{username}:{password}@{host}:{port}/{database}"
def _get_mysql_url(self):
"""Get MySQL/MariaDB connection URL."""
host = os.getenv('MYSQL_HOST', 'localhost')
port = os.getenv('MYSQL_PORT', '3306')
database = os.getenv('MYSQL_DATABASE', 'hockey_results')
username = os.getenv('MYSQL_USER', 'root')
password = os.getenv('MYSQL_PASSWORD', '')
charset = os.getenv('MYSQL_CHARSET', 'utf8mb4')
return f"mysql+pymysql://{username}:{password}@{host}:{port}/{database}?charset={charset}"
def _get_sqlite_url(self):
"""Get SQLite connection URL."""
database_path = os.getenv('SQLITE_DATABASE_PATH', 'hockey_results.db')
return f"sqlite:///{database_path}"
def get_session(self):
"""Get database session."""
return self.SessionLocal()
def create_tables(self):
"""Create all database tables."""
Base.metadata.create_all(bind=self.engine)
# Global database configuration instance
db_config = DatabaseConfig()
# Database Models
class Player(Base):
"""Player model."""
__tablename__ = 'players'
player_number = Column(Integer, primary_key=True)
player_forenames = Column(String(50))
player_surname = Column(String(30))
player_nickname = Column(String(30))
player_chinese_name = Column(String(10))
player_email = Column(String(255))
player_dob = Column(Date)
player_hkid = Column(String(20))
player_tel_number = Column(String(30))
player_team = Column(String(6))
player_picture_url = Column(String(255))
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class Club(Base):
"""Club model."""
__tablename__ = 'clubs'
id = Column(Integer, primary_key=True)
hockey_club = Column(String(100), unique=True, nullable=False)
logo_url = Column(String(255))
created_at = Column(DateTime, default=datetime.utcnow)
class Team(Base):
"""Team model."""
__tablename__ = 'teams'
id = Column(Integer, primary_key=True)
club = Column(String(100), ForeignKey('clubs.hockey_club'))
team = Column(String(10))
display_name = Column(String(100))
league = Column(String(50))
created_at = Column(DateTime, default=datetime.utcnow)
class MatchSquad(Base):
"""Match squad model."""
__tablename__ = 'match_squad'
id = Column(Integer, primary_key=True)
player_number = Column(Integer, ForeignKey('players.player_number'))
player_forenames = Column(String(50))
player_surname = Column(String(30))
player_nickname = Column(String(30))
match_date = Column(Date)
created_at = Column(DateTime, default=datetime.utcnow)
class HockeyFixture(Base):
"""Hockey fixture model."""
__tablename__ = 'hockey_fixtures'
fixture_number = Column(Integer, primary_key=True)
date = Column(Date)
time = Column(String(10))
home_team = Column(String(100))
away_team = Column(String(100))
venue = Column(String(255))
home_score = Column(Integer)
away_score = Column(Integer)
umpire1 = Column(String(100))
umpire2 = Column(String(100))
match_official = Column(String(100))
division = Column(String(50))
created_at = Column(DateTime, default=datetime.utcnow)
class AdminSettings(Base):
"""Admin settings model."""
__tablename__ = 'admin_settings'
id = Column(Integer, primary_key=True)
userid = Column(String(50), default='admin')
next_fixture = Column(Integer)
next_club = Column(String(100))
next_team = Column(String(100))
next_date = Column(Date)
curr_motm = Column(Integer, ForeignKey('players.player_number'))
curr_dotd = Column(Integer, ForeignKey('players.player_number'))
oppo_logo = Column(String(255))
hkfc_logo = Column(String(255))
motm_url_suffix = Column(String(50))
prev_fixture = Column(Integer)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class MotmVote(Base):
"""MOTM/DotD voting model."""
__tablename__ = 'motm_votes'
id = Column(Integer, primary_key=True)
player_number = Column(Integer, ForeignKey('players.player_number'))
player_name = Column(String(100))
motm_total = Column(Integer, default=0)
dotd_total = Column(Integer, default=0)
goals_total = Column(Integer, default=0)
assists_total = Column(Integer, default=0)
fixture_number = Column(Integer)
motm_votes = Column(Integer, default=0)
dotd_votes = Column(Integer, default=0)
goals = Column(Integer, default=0)
assists = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class MatchComment(Base):
"""Match comments model."""
__tablename__ = 'match_comments'
id = Column(Integer, primary_key=True)
match_date = Column(Date)
opposition = Column(String(100))
comment = Column(Text)
created_at = Column(DateTime, default=datetime.utcnow)
class HockeyUser(Base):
"""User authentication model."""
__tablename__ = 'hockey_users'
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True, nullable=False)
email = Column(String(255), unique=True, nullable=False)
password = Column(String(255), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
last_login = Column(DateTime)
# Database utility functions
def get_db_session():
"""Get database session."""
return db_config.get_session()
def execute_sql(sql_command, params=None):
"""Execute SQL command with parameters."""
session = get_db_session()
try:
if params:
result = session.execute(sql_command, params)
else:
result = session.execute(sql_command)
session.commit()
return result
except Exception as e:
session.rollback()
print(f"SQL Error: {e}")
raise
finally:
session.close()
def fetch_all(sql_command, params=None):
"""Fetch all results from SQL query."""
session = get_db_session()
try:
if params:
result = session.execute(sql_command, params)
else:
result = session.execute(sql_command)
rows = result.fetchall()
# Convert to list of dictionaries for compatibility
return [dict(row._mapping) for row in rows] if rows else []
except Exception as e:
print(f"SQL Error: {e}")
return []
finally:
session.close()
def fetch_one(sql_command, params=None):
"""Fetch one result from SQL query."""
session = get_db_session()
try:
if params:
result = session.execute(sql_command, params)
else:
result = session.execute(sql_command)
row = result.fetchone()
return dict(row._mapping) if row else None
except Exception as e:
print(f"SQL Error: {e}")
return None
finally:
session.close()
# Legacy compatibility functions
def sql_write(sql_cmd, params=None):
"""Legacy compatibility function for sql_write."""
try:
execute_sql(sql_cmd, params)
return True
except Exception as e:
print(f"Write Error: {e}")
return False
def sql_write_static(sql_cmd, params=None):
"""Legacy compatibility function for sql_write_static."""
return sql_write(sql_cmd, params)
def sql_read(sql_cmd, params=None):
"""Legacy compatibility function for sql_read."""
try:
result = fetch_all(sql_cmd, params)
return result
except Exception as e:
print(f"Read Error: {e}")
return []
def sql_read_static(sql_cmd, params=None):
"""Legacy compatibility function for sql_read_static."""
return sql_read(sql_cmd, params)
# Initialize database tables
def init_database():
"""Initialize database tables."""
try:
db_config.create_tables()
print("Database tables created successfully")
except Exception as e:
print(f"Database initialization error: {e}")
raise

View File

@ -1,19 +0,0 @@
[DATABASE]
type = postgresql
sqlite_database_path = hockey_results.db
[MYSQL]
host = localhost
port = 3306
database = hockey_results
username = root
password =
charset = utf8mb4
[POSTGRESQL]
host = icarus.ipa.champion
port = 5432
database = motm
username = motm_user
password = q7y7f7Lv*sODJZ2wGiv0Wq5a

View File

@ -1,116 +0,0 @@
# encoding=utf-8
"""
Database configuration module with SQLAlchemy support.
This module provides backward compatibility with the old PyMySQL-based functions
while using SQLAlchemy for database operations.
"""
import os
import warnings
from database import (
db_config,
sql_write,
sql_write_static,
sql_read,
sql_read_static,
get_db_session,
execute_sql,
fetch_all,
fetch_one,
init_database
)
# Legacy constants for backward compatibility
CLOUDSQL_CONNECTION_NAME = os.getenv('CLOUDSQL_CONNECTION_NAME', "hk-hockey:asia-east2:hk-hockey-sql")
LOCAL_DB_SERVER = os.getenv('LOCAL_DB_SERVER', "mariadb.db.svc.cluster.local")
CLOUDSQL_USER = os.getenv('CLOUDSQL_USER', "root")
CLOUDSQL_WRITE_USER = os.getenv('CLOUDSQL_WRITE_USER', "hockeyWrite")
CLOUDSQL_READ_USER = os.getenv('CLOUDSQL_READ_USER', "hockeyRead")
CLOUDSQL_PASSWORD = os.getenv('CLOUDSQL_PASSWORD', "P8P1YopMlwg8TxhE")
CLOUDSQL_WRITE_PASSWORD = os.getenv('CLOUDSQL_WRITE_PASSWORD', "1URYcxXXlQ6xOWgj")
CLOUDSQL_READ_PASSWORD = os.getenv('CLOUDSQL_READ_PASSWORD', "o4GWrbbkBKy3oR6u")
CLOUDSQL_DATABASE = os.getenv('CLOUDSQL_DATABASE', "20209_hockeyResults")
LOCAL_DATABASE = os.getenv('LOCAL_DATABASE', "hockeyResults2021")
CLOUDSQL_DATABASE_STATIC = os.getenv('CLOUDSQL_DATABASE_STATIC', "hockeyResults")
CLOUDSQL_CHARSET = os.getenv('CLOUDSQL_CHARSET', "utf8")
# Legacy functions for backward compatibility
def write_cloudsql():
"""
Legacy function - now uses SQLAlchemy.
Returns a session object for compatibility.
"""
warnings.warn(
"write_cloudsql() is deprecated. Use get_db_session() instead.",
DeprecationWarning,
stacklevel=2
)
return get_db_session()
def write_cloudsql_static():
"""
Legacy function - now uses SQLAlchemy.
Returns a session object for compatibility.
"""
warnings.warn(
"write_cloudsql_static() is deprecated. Use get_db_session() instead.",
DeprecationWarning,
stacklevel=2
)
return get_db_session()
def read_cloudsql():
"""
Legacy function - now uses SQLAlchemy.
Returns a session object for compatibility.
"""
warnings.warn(
"read_cloudsql() is deprecated. Use get_db_session() instead.",
DeprecationWarning,
stacklevel=2
)
return get_db_session()
def read_cloudsql_static():
"""
Legacy function - now uses SQLAlchemy.
Returns a session object for compatibility.
"""
warnings.warn(
"read_cloudsql_static() is deprecated. Use get_db_session() instead.",
DeprecationWarning,
stacklevel=2
)
return get_db_session()
# These functions now use SQLAlchemy but maintain the same interface
__all__ = [
'sql_write',
'sql_write_static',
'sql_read',
'sql_read_static',
'get_db_session',
'execute_sql',
'fetch_all',
'fetch_one',
'init_database',
'db_config',
# Legacy constants
'CLOUDSQL_CONNECTION_NAME',
'LOCAL_DB_SERVER',
'CLOUDSQL_USER',
'CLOUDSQL_WRITE_USER',
'CLOUDSQL_READ_USER',
'CLOUDSQL_PASSWORD',
'CLOUDSQL_WRITE_PASSWORD',
'CLOUDSQL_READ_PASSWORD',
'CLOUDSQL_DATABASE',
'LOCAL_DATABASE',
'CLOUDSQL_DATABASE_STATIC',
'CLOUDSQL_CHARSET',
# Legacy functions
'write_cloudsql',
'write_cloudsql_static',
'read_cloudsql',
'read_cloudsql_static'
]

View File

@ -1,310 +0,0 @@
# encoding=utf-8
"""
Database setup and configuration management module.
Handles database initialization, configuration saving/loading, and sample data creation.
"""
import os
import json
import configparser
from datetime import datetime
from sqlalchemy import create_engine, text
from sqlalchemy.exc import SQLAlchemyError
from database import Base, db_config, init_database
from database import (
Player, Club, Team, MatchSquad, HockeyFixture,
AdminSettings, MotmVote, MatchComment, HockeyUser
)
class DatabaseConfigManager:
"""Manages database configuration and setup."""
def __init__(self, config_file='database_config.ini'):
# Use absolute path to ensure we save in the right location
if not os.path.isabs(config_file):
config_file = os.path.join(os.path.dirname(__file__), config_file)
self.config_file = config_file
self.config = configparser.ConfigParser()
self.load_config()
def load_config(self):
"""Load configuration from file."""
if os.path.exists(self.config_file):
self.config.read(self.config_file)
else:
# Create default configuration
self.config['DATABASE'] = {
'type': 'sqlite',
'sqlite_database_path': 'hockey_results.db'
}
self.config['MYSQL'] = {
'host': 'localhost',
'port': '3306',
'database': 'hockey_results',
'username': 'root',
'password': '',
'charset': 'utf8mb4'
}
self.config['POSTGRESQL'] = {
'host': 'localhost',
'port': '5432',
'database': 'hockey_results',
'username': 'postgres',
'password': ''
}
# Save the default configuration to file
self._save_config_file()
def save_config(self, form_data):
"""Save configuration from form data."""
# Update database type
self.config['DATABASE']['type'] = form_data['database_type']
# Update SQLite settings
if 'sqlite_database_path' in form_data:
self.config['DATABASE']['sqlite_database_path'] = form_data['sqlite_database_path']
# Update MySQL settings
if 'mysql_host' in form_data and form_data['mysql_host'] is not None:
self.config['MYSQL']['host'] = str(form_data['mysql_host'])
if 'mysql_port' in form_data and form_data['mysql_port'] is not None:
self.config['MYSQL']['port'] = str(form_data['mysql_port'])
if 'mysql_database' in form_data and form_data['mysql_database'] is not None:
self.config['MYSQL']['database'] = str(form_data['mysql_database'])
if 'mysql_username' in form_data and form_data['mysql_username'] is not None:
self.config['MYSQL']['username'] = str(form_data['mysql_username'])
if 'mysql_password' in form_data and form_data['mysql_password'] is not None:
self.config['MYSQL']['password'] = str(form_data['mysql_password'])
if 'mysql_charset' in form_data and form_data['mysql_charset'] is not None:
self.config['MYSQL']['charset'] = str(form_data['mysql_charset'])
# Update PostgreSQL settings
if 'postgres_host' in form_data and form_data['postgres_host'] is not None:
self.config['POSTGRESQL']['host'] = str(form_data['postgres_host'])
if 'postgres_port' in form_data and form_data['postgres_port'] is not None:
self.config['POSTGRESQL']['port'] = str(form_data['postgres_port'])
if 'postgres_database' in form_data and form_data['postgres_database'] is not None:
self.config['POSTGRESQL']['database'] = str(form_data['postgres_database'])
if 'postgres_username' in form_data and form_data['postgres_username'] is not None:
self.config['POSTGRESQL']['username'] = str(form_data['postgres_username'])
if 'postgres_password' in form_data and form_data['postgres_password'] is not None:
self.config['POSTGRESQL']['password'] = str(form_data['postgres_password'])
# Save to file
self._save_config_file()
# Update environment variables
self._update_environment_variables()
def _save_config_file(self):
"""Save configuration to file."""
with open(self.config_file, 'w') as f:
self.config.write(f)
def _update_environment_variables(self):
"""Update environment variables based on configuration."""
db_type = self.config['DATABASE']['type']
if db_type == 'sqlite':
os.environ['DATABASE_TYPE'] = 'sqlite'
os.environ['SQLITE_DATABASE_PATH'] = self.config['DATABASE']['sqlite_database_path']
elif db_type == 'mysql':
os.environ['DATABASE_TYPE'] = 'mysql'
os.environ['MYSQL_HOST'] = self.config['MYSQL']['host']
os.environ['MYSQL_PORT'] = self.config['MYSQL']['port']
os.environ['MYSQL_DATABASE'] = self.config['MYSQL']['database']
os.environ['MYSQL_USER'] = self.config['MYSQL']['username']
os.environ['MYSQL_PASSWORD'] = self.config['MYSQL']['password']
os.environ['MYSQL_CHARSET'] = self.config['MYSQL']['charset']
elif db_type == 'postgresql':
os.environ['DATABASE_TYPE'] = 'postgresql'
os.environ['POSTGRES_HOST'] = self.config['POSTGRESQL']['host']
os.environ['POSTGRES_PORT'] = self.config['POSTGRESQL']['port']
os.environ['POSTGRES_DATABASE'] = self.config['POSTGRESQL']['database']
os.environ['POSTGRES_USER'] = self.config['POSTGRESQL']['username']
os.environ['POSTGRES_PASSWORD'] = self.config['POSTGRESQL']['password']
def test_connection(self, form_data):
"""Test database connection with provided settings."""
try:
# Temporarily update environment variables
old_env = {}
for key in ['DATABASE_TYPE', 'SQLITE_DATABASE_PATH', 'MYSQL_HOST', 'MYSQL_PORT',
'MYSQL_DATABASE', 'MYSQL_USER', 'MYSQL_PASSWORD', 'MYSQL_CHARSET',
'POSTGRES_HOST', 'POSTGRES_PORT', 'POSTGRES_DATABASE', 'POSTGRES_USER', 'POSTGRES_PASSWORD']:
old_env[key] = os.environ.get(key)
# Set new environment variables
os.environ['DATABASE_TYPE'] = form_data['database_type']
if form_data['database_type'] == 'sqlite':
os.environ['SQLITE_DATABASE_PATH'] = form_data['sqlite_database_path']
elif form_data['database_type'] == 'mysql':
os.environ['MYSQL_HOST'] = form_data['mysql_host']
os.environ['MYSQL_PORT'] = str(form_data['mysql_port'])
os.environ['MYSQL_DATABASE'] = form_data['mysql_database']
os.environ['MYSQL_USER'] = form_data['mysql_username']
os.environ['MYSQL_PASSWORD'] = form_data['mysql_password']
os.environ['MYSQL_CHARSET'] = form_data['mysql_charset']
elif form_data['database_type'] == 'postgresql':
os.environ['POSTGRES_HOST'] = form_data['postgres_host']
os.environ['POSTGRES_PORT'] = str(form_data['postgres_port'])
os.environ['POSTGRES_DATABASE'] = form_data['postgres_database']
os.environ['POSTGRES_USER'] = form_data['postgres_username']
os.environ['POSTGRES_PASSWORD'] = form_data['postgres_password']
# Test connection
from database import DatabaseConfig
test_config = DatabaseConfig()
engine = test_config.engine
with engine.connect() as conn:
result = conn.execute(text("SELECT 1"))
result.fetchone()
return True, "Connection successful!"
except Exception as e:
return False, f"Connection failed: {str(e)}"
finally:
# Restore original environment variables
for key, value in old_env.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
def initialize_database(self, create_sample_data=True):
"""Initialize database with tables and optionally sample data."""
try:
# Create tables
init_database()
if create_sample_data:
self._create_sample_data()
return True, "Database initialized successfully!"
except Exception as e:
return False, f"Database initialization failed: {str(e)}"
def _create_sample_data(self):
"""Create sample data for testing."""
from database import get_db_session
session = get_db_session()
try:
# Create sample clubs (only if they don't exist)
clubs_data = [
{'hockey_club': 'HKFC', 'logo_url': '/static/images/hkfc_logo.png'},
{'hockey_club': 'KCC', 'logo_url': '/static/images/kcc_logo.png'},
{'hockey_club': 'USRC', 'logo_url': '/static/images/usrc_logo.png'},
{'hockey_club': 'Valley', 'logo_url': '/static/images/valley_logo.png'},
]
for club_data in clubs_data:
# Check if club already exists
existing_club = session.query(Club).filter_by(hockey_club=club_data['hockey_club']).first()
if not existing_club:
club = Club(**club_data)
session.add(club)
# Create sample teams (only if they don't exist)
teams_data = [
{'club': 'HKFC', 'team': 'A', 'display_name': 'HKFC A', 'league': 'Premier Division'},
{'club': 'HKFC', 'team': 'B', 'display_name': 'HKFC B', 'league': 'Division 1'},
{'club': 'HKFC', 'team': 'C', 'display_name': 'HKFC C', 'league': 'Division 2'},
]
for team_data in teams_data:
# Check if team already exists
existing_team = session.query(Team).filter_by(club=team_data['club'], team=team_data['team']).first()
if not existing_team:
team = Team(**team_data)
session.add(team)
# Create sample players (only if they don't exist)
players_data = [
{'player_number': 1, 'player_forenames': 'John', 'player_surname': 'Smith', 'player_nickname': 'Smithers', 'player_team': 'HKFC C'},
{'player_number': 2, 'player_forenames': 'Mike', 'player_surname': 'Jones', 'player_nickname': 'Jonesy', 'player_team': 'HKFC C'},
{'player_number': 3, 'player_forenames': 'David', 'player_surname': 'Brown', 'player_nickname': 'Brownie', 'player_team': 'HKFC C'},
{'player_number': 4, 'player_forenames': 'Chris', 'player_surname': 'Wilson', 'player_nickname': 'Willy', 'player_team': 'HKFC C'},
{'player_number': 5, 'player_forenames': 'Tom', 'player_surname': 'Taylor', 'player_nickname': 'Tayls', 'player_team': 'HKFC C'},
]
for player_data in players_data:
# Check if player already exists
existing_player = session.query(Player).filter_by(player_number=player_data['player_number']).first()
if not existing_player:
player = Player(**player_data)
session.add(player)
# Create sample admin settings (only if they don't exist)
existing_admin = session.query(AdminSettings).filter_by(userid='admin').first()
if not existing_admin:
admin_settings = AdminSettings(
userid='admin',
next_fixture=1,
next_club='KCC',
next_team='KCC A',
curr_motm=1,
curr_dotd=2,
oppo_logo='/static/images/kcc_logo.png',
hkfc_logo='/static/images/hkfc_logo.png',
motm_url_suffix='abc123',
prev_fixture=0
)
session.add(admin_settings)
# Create sample fixtures (only if they don't exist)
fixtures_data = [
{'fixture_number': 1, 'date': datetime(2024, 1, 15), 'home_team': 'HKFC C', 'away_team': 'KCC A', 'venue': 'HKFC'},
{'fixture_number': 2, 'date': datetime(2024, 1, 22), 'home_team': 'USRC A', 'away_team': 'HKFC C', 'venue': 'USRC'},
{'fixture_number': 3, 'date': datetime(2024, 1, 29), 'home_team': 'HKFC C', 'away_team': 'Valley A', 'venue': 'HKFC'},
]
for fixture_data in fixtures_data:
# Check if fixture already exists
existing_fixture = session.query(HockeyFixture).filter_by(fixture_number=fixture_data['fixture_number']).first()
if not existing_fixture:
fixture = HockeyFixture(**fixture_data)
session.add(fixture)
session.commit()
except Exception as e:
session.rollback()
raise e
finally:
session.close()
def get_config_dict(self):
"""Get configuration as dictionary for form population."""
config_dict = {}
# Database type
config_dict['database_type'] = self.config['DATABASE'].get('type', 'sqlite')
config_dict['sqlite_database_path'] = self.config['DATABASE'].get('sqlite_database_path', 'hockey_results.db')
# MySQL settings
config_dict['mysql_host'] = self.config['MYSQL'].get('host', 'localhost')
config_dict['mysql_port'] = int(self.config['MYSQL'].get('port', '3306'))
config_dict['mysql_database'] = self.config['MYSQL'].get('database', 'hockey_results')
config_dict['mysql_username'] = self.config['MYSQL'].get('username', 'root')
config_dict['mysql_password'] = self.config['MYSQL'].get('password', '')
config_dict['mysql_charset'] = self.config['MYSQL'].get('charset', 'utf8mb4')
# PostgreSQL settings
config_dict['postgres_host'] = self.config['POSTGRESQL'].get('host', 'localhost')
config_dict['postgres_port'] = int(self.config['POSTGRESQL'].get('port', '5432'))
config_dict['postgres_database'] = self.config['POSTGRESQL'].get('database', 'hockey_results')
config_dict['postgres_username'] = self.config['POSTGRESQL'].get('username', 'postgres')
config_dict['postgres_password'] = self.config['POSTGRESQL'].get('password', '')
return config_dict
# Global instance
db_config_manager = DatabaseConfigManager()

View File

@ -1,64 +0,0 @@
#!/usr/bin/env python3
"""
Deployment script for MOTM Flask application to Google App Engine.
"""
import os
import subprocess
import sys
def deploy_to_app_engine():
"""Deploy the MOTM application to Google App Engine."""
print("🚀 Starting deployment to Google App Engine...")
# Check if gcloud is installed
try:
subprocess.run(['gcloud', '--version'], check=True, capture_output=True)
print("✓ Google Cloud SDK is installed")
except (subprocess.CalledProcessError, FileNotFoundError):
print("❌ Google Cloud SDK not found. Please install it first:")
print(" https://cloud.google.com/sdk/docs/install")
return False
# Check if user is authenticated
try:
subprocess.run(['gcloud', 'auth', 'list', '--filter=status:ACTIVE'], check=True, capture_output=True)
print("✓ Google Cloud authentication verified")
except subprocess.CalledProcessError:
print("❌ Not authenticated with Google Cloud. Run 'gcloud auth login' first")
return False
# Check if app.yaml exists
if not os.path.exists('app.yaml'):
print("❌ app.yaml not found in current directory")
return False
print("✓ app.yaml found")
# Deploy the application
try:
print("📦 Deploying application...")
result = subprocess.run(['gcloud', 'app', 'deploy'], check=True)
print("✅ Deployment completed successfully!")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Deployment failed: {e}")
return False
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == '--deploy':
success = deploy_to_app_engine()
sys.exit(0 if success else 1)
else:
print("MOTM Application Deployment Script")
print("=================================")
print()
print("Usage:")
print(" python deploy.py --deploy # Deploy to Google App Engine")
print()
print("Prerequisites:")
print(" 1. Install Google Cloud SDK")
print(" 2. Run 'gcloud auth login'")
print(" 3. Set your project: 'gcloud config set project YOUR_PROJECT_ID'")
print(" 4. Ensure app.yaml is configured correctly")

View File

@ -1,71 +0,0 @@
version: '3.8'
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: motm-postgres
environment:
POSTGRES_DB: motm_db
POSTGRES_USER: motm_user
POSTGRES_PASSWORD: motm_password
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U motm_user -d motm_db"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
# MOTM Application
motm-app:
build:
context: .
dockerfile: Containerfile
container_name: motm-app
environment:
# Database configuration
DATABASE_TYPE: postgresql
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: motm_db
DB_USER: motm_user
DB_PASSWORD: motm_password
# Flask configuration
FLASK_ENV: production
FLASK_APP: main.py
FLASK_RUN_HOST: 0.0.0.0
FLASK_RUN_PORT: 5000
# Security
SECRET_KEY: your-secret-key-change-this-in-production
ports:
- "5000:5000"
depends_on:
postgres:
condition: service_healthy
volumes:
- ./data:/app/data
- ./logs:/app/logs
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes:
postgres_data:
driver: local
networks:
default:
name: motm-network

View File

@ -1,328 +0,0 @@
# encoding=utf-8
"""
Fixture scraper for Hong Kong Hockey Association website
Fetches upcoming HKFC C team fixtures from https://hockey.org.hk/MenFixture.asp
"""
import requests
from bs4 import BeautifulSoup
from datetime import datetime
import re
class FixtureScraper:
"""Scrapes fixture data from Hong Kong Hockey Association website"""
FIXTURE_URL = "https://hockey.org.hk/MenFixture.asp"
TARGET_TEAM = "HKFC C"
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
})
def fetch_fixtures(self):
"""Fetch and parse fixtures from the website"""
try:
response = self.session.get(self.FIXTURE_URL, timeout=10)
response.raise_for_status()
return self._parse_fixtures(response.text)
except requests.RequestException as e:
print(f"Error fetching fixtures: {e}")
return []
def _parse_fixtures(self, html_content):
"""Parse HTML content and extract fixture information"""
soup = BeautifulSoup(html_content, 'lxml')
fixtures = []
# Find all table rows
rows = soup.find_all('tr')
current_date = None
for row in rows:
# Check if this row contains a date header
date_cells = row.find_all('td', colspan=True)
if date_cells:
date_text = date_cells[0].get_text(strip=True)
# Extract date from text like "Sunday, 7 Sep 2025"
date_match = re.search(r'(\w+day),\s+(\d+)\s+(\w+)\s+(\d{4})', date_text)
if date_match:
try:
day_name, day, month, year = date_match.groups()
date_str = f"{day} {month} {year}"
current_date = datetime.strptime(date_str, "%d %b %Y").date()
except ValueError:
continue
continue
# Check if this row contains fixture data
cells = row.find_all('td')
if len(cells) >= 5 and current_date:
try:
# Extract fixture details
# Note: The first cell might be empty or contain status (C/P)
# Column order: [Status/Division], Division, Time, Venue, Home, Away, [Umpire columns...]
# Handle tables with or without status column
if len(cells) >= 6:
# If 6+ columns, likely has status column first
status_or_div = cells[0].get_text(strip=True)
division = cells[1].get_text(strip=True) if cells[1] else ""
time = cells[2].get_text(strip=True) if cells[2] else ""
venue = cells[3].get_text(strip=True) if cells[3] else ""
home_team = cells[4].get_text(strip=True) if cells[4] else ""
away_team = cells[5].get_text(strip=True) if cells[5] else ""
else:
# If 5 columns, no status column
division = cells[0].get_text(strip=True) if cells[0] else ""
time = cells[1].get_text(strip=True) if cells[1] else ""
venue = cells[2].get_text(strip=True) if cells[2] else ""
home_team = cells[3].get_text(strip=True) if cells[3] else ""
away_team = cells[4].get_text(strip=True) if cells[4] else ""
# Check if HKFC C is playing in this match
if self.TARGET_TEAM in home_team or self.TARGET_TEAM in away_team:
# Determine opponent
if self.TARGET_TEAM in home_team:
opponent = away_team
is_home = True
else:
opponent = home_team
is_home = False
fixture = {
'date': current_date,
'time': time,
'venue': venue,
'opponent': opponent,
'is_home': is_home,
'home_team': home_team,
'away_team': away_team,
'division': division
}
fixtures.append(fixture)
except (IndexError, AttributeError) as e:
# Skip malformed rows
continue
return fixtures
def get_next_fixture(self):
"""Get the next upcoming HKFC C fixture"""
fixtures = self.fetch_fixtures()
if not fixtures:
return None
# Filter for future fixtures and sort by date
today = datetime.now().date()
future_fixtures = [f for f in fixtures if f['date'] >= today]
if not future_fixtures:
return None
# Sort by date and return the earliest
future_fixtures.sort(key=lambda x: x['date'])
return future_fixtures[0]
def get_all_future_fixtures(self, limit=10):
"""Get all future HKFC C fixtures, optionally limited"""
fixtures = self.fetch_fixtures()
if not fixtures:
return []
# Filter for future fixtures and sort by date
today = datetime.now().date()
future_fixtures = [f for f in fixtures if f['date'] >= today]
future_fixtures.sort(key=lambda x: x['date'])
return future_fixtures[:limit] if limit else future_fixtures
def get_next_hkfc_c_fixture():
"""Convenience function to get the next HKFC C fixture"""
scraper = FixtureScraper()
return scraper.get_next_fixture()
def get_opponent_club_name(opponent_team):
"""Extract club name from opponent team name (e.g., 'KCC B' -> 'KCC')"""
if not opponent_team:
return None
# Common patterns: "Club Letter" (e.g., "KCC B", "Valley A")
# Remove team letters and common suffixes
club_name = re.sub(r'\s+[A-H]$', '', opponent_team).strip()
return club_name
def match_opponent_to_club(opponent_team, clubs_database=None):
"""
Match an opponent team name to a club in the database
Args:
opponent_team (str): The opponent team name (e.g., "KCC B", "Valley A")
clubs_database (list): List of clubs from database, if None will fetch from DB
Returns:
dict: Club information if matched, None if no match found
"""
if not opponent_team:
return None
# Import here to avoid circular imports
try:
from db_config import sql_read
from sqlalchemy import text
except ImportError:
return None
# Get clubs from database if not provided
if clubs_database is None:
try:
clubs_result = sql_read(text("SELECT hockey_club FROM clubs ORDER BY hockey_club"))
clubs_database = [club['hockey_club'] for club in clubs_result] if clubs_result else []
except:
clubs_database = []
# Extract potential club name from opponent team
potential_club_names = []
# Method 1: Remove team letters (A, B, C, etc.)
base_name = re.sub(r'\s+[A-H]$', '', opponent_team).strip()
potential_club_names.append(base_name)
# Method 2: Remove common suffixes
suffixes_to_remove = [' A', ' B', ' C', ' D', ' E', ' F', ' G', ' H', ' I', ' J']
for suffix in suffixes_to_remove:
if opponent_team.endswith(suffix):
potential_club_names.append(opponent_team[:-len(suffix)].strip())
# Method 3: Split on spaces and try different combinations
words = opponent_team.split()
if len(words) > 1:
# Try first word only
potential_club_names.append(words[0])
# Try first two words
if len(words) > 2:
potential_club_names.append(' '.join(words[:2]))
# Try to match against database clubs
for potential_name in potential_club_names:
# Exact match
for club in clubs_database:
if club.lower() == potential_name.lower():
return {
'club_name': club,
'match_type': 'exact',
'confidence': 'high'
}
# Partial match (club name contains the potential name)
for club in clubs_database:
if potential_name.lower() in club.lower() or club.lower() in potential_name.lower():
return {
'club_name': club,
'match_type': 'partial',
'confidence': 'medium'
}
# If no match found, return the best guess
best_guess = potential_club_names[0] if potential_club_names else opponent_team
return {
'club_name': best_guess,
'match_type': 'guess',
'confidence': 'low'
}
def get_opponent_club_info(opponent_team):
"""
Get full club information for an opponent team
Args:
opponent_team (str): The opponent team name
Returns:
dict: Full club information including logo URL, or None if not found
"""
if not opponent_team:
return None
try:
from db_config import sql_read
from sqlalchemy import text
except ImportError:
return None
# First, try to match the opponent to a club
match_result = match_opponent_to_club(opponent_team)
if not match_result:
return None
club_name = match_result['club_name']
# Get full club information from database
try:
sql = text("SELECT id, hockey_club, logo_url FROM clubs WHERE hockey_club = :club_name")
club_info = sql_read(sql, {'club_name': club_name})
if club_info:
club_data = club_info[0]
return {
'id': club_data['id'],
'club_name': club_data['hockey_club'],
'logo_url': club_data['logo_url'],
'match_result': match_result
}
else:
# Club not found in database, return match result only
return {
'club_name': club_name,
'logo_url': None,
'match_result': match_result
}
except Exception as e:
print(f"Error getting club info: {e}")
return None
if __name__ == "__main__":
# Test the scraper
print("Testing Hong Kong Hockey Fixture Scraper...")
print("=" * 60)
scraper = FixtureScraper()
print("\nFetching next HKFC C fixture...")
next_fixture = scraper.get_next_fixture()
if next_fixture:
print(f"\nNext HKFC C Match:")
print(f" Date: {next_fixture['date'].strftime('%A, %d %B %Y')}")
print(f" Time: {next_fixture['time']}")
print(f" Venue: {next_fixture['venue']}")
print(f" Opponent: {next_fixture['opponent']}")
print(f" Home/Away: {'Home' if next_fixture['is_home'] else 'Away'}")
print(f" Division: {next_fixture['division']}")
club_name = get_opponent_club_name(next_fixture['opponent'])
print(f" Opponent Club: {club_name}")
else:
print("\nNo upcoming fixtures found.")
print("\n" + "=" * 60)
print("\nFetching next 5 HKFC C fixtures...")
future_fixtures = scraper.get_all_future_fixtures(limit=5)
if future_fixtures:
for i, fixture in enumerate(future_fixtures, 1):
print(f"\n{i}. {fixture['date'].strftime('%d %b %Y')} vs {fixture['opponent']} ({fixture['venue']})")
else:
print("\nNo upcoming fixtures found.")

View File

@ -1,186 +0,0 @@
# encoding=utf-8
from flask_wtf import FlaskForm
from wtforms import BooleanField, StringField, PasswordField, IntegerField, TextAreaField, SubmitField, RadioField, SelectField, DateField, FieldList
from wtforms_components import read_only
from wtforms import validators, ValidationError
from wtforms.validators import InputRequired, Email, Length
from datetime import datetime
class motmForm(FlaskForm):
startDate = DateField('DatePicker', format='%d-%m-%Y')
endDate = DateField('DatePicker', format='%d-%m-%Y')
class motmAdminForm(FlaskForm):
startDate = DateField('DatePicker', format='%d-%m-%Y')
endDate = DateField('DatePicker', format='%d-%m-%Y')
class adminSettingsForm2(FlaskForm):
nextMatchDate = DateField('Match Date', format='%Y-%m-%d')
nextOppoClub = StringField('Next Opposition Club:')
nextOppoTeam = StringField("Next Opposition Team:")
currMotM = SelectField('Current Man of the Match:', choices=[])
currDotD = SelectField('Current Dick of the Day:', choices=[])
saveButton = SubmitField('Save Settings')
activateButton = SubmitField('Activate MotM Vote')
class goalsAssistsForm(FlaskForm):
fixtureNumber = StringField('Fixture Number')
match = SelectField('Fixture')
homeTeam = StringField('Home Team')
awayTeam = StringField('Away Team')
playerNumber = StringField('Player Number')
playerName = StringField('Player Name')
assists = SelectField('Assists:', choices=[(0, '0'), (1, '1'), (2, '2'), (3, '3'), (4, '4')])
goals = SelectField('Goals:', choices=[(0, '0'), (1, '1'), (2, '2'), (3, '3'), (4, '4')])
submit = SubmitField('Submit')
class DatabaseSetupForm(FlaskForm):
"""Form for database setup and configuration."""
database_type = SelectField('Database Type',
choices=[('sqlite', 'SQLite'), ('mysql', 'MySQL/MariaDB'), ('postgresql', 'PostgreSQL')],
validators=[InputRequired()])
# SQLite fields
sqlite_database_path = StringField('Database File Path',
default='hockey_results.db')
# MySQL/MariaDB fields
mysql_host = StringField('Host', default='localhost')
mysql_port = IntegerField('Port', default=3306)
mysql_database = StringField('Database Name', default='hockey_results')
mysql_username = StringField('Username', default='root')
mysql_password = PasswordField('Password')
mysql_charset = StringField('Charset', default='utf8mb4')
# PostgreSQL fields
postgres_host = StringField('Host', default='localhost')
postgres_port = IntegerField('Port', default=5432)
postgres_database = StringField('Database Name', default='hockey_results')
postgres_username = StringField('Username', default='postgres')
postgres_password = PasswordField('Password')
# Setup options
create_sample_data = BooleanField('Create Sample Data', default=True)
initialize_tables = BooleanField('Initialize Database Tables', default=True)
# Action buttons
test_connection = SubmitField('Test Connection')
save_config = SubmitField('Save Configuration')
initialize_database = SubmitField('Initialize Database')
class PlayerForm(FlaskForm):
"""Form for adding/editing players."""
player_number = IntegerField('Player Number', validators=[InputRequired()])
player_forenames = StringField('First Names', validators=[InputRequired()])
player_surname = StringField('Surname', validators=[InputRequired()])
player_nickname = StringField('Nickname', validators=[InputRequired()])
player_team = SelectField('Team',
choices=[('HKFC A', 'HKFC A'),
('HKFC B', 'HKFC B'),
('HKFC C', 'HKFC C')],
default='HKFC C')
# Action buttons
save_player = SubmitField('Save Player')
cancel = SubmitField('Cancel')
class ClubForm(FlaskForm):
"""Form for adding/editing clubs."""
hockey_club = StringField('Club Name', validators=[InputRequired()])
logo_url = StringField('Logo URL', validators=[InputRequired()])
# Action buttons
save_club = SubmitField('Save Club')
cancel = SubmitField('Cancel')
class TeamForm(FlaskForm):
"""Form for adding/editing teams."""
club = SelectField('Club', validators=[InputRequired()], choices=[], coerce=str)
team = StringField('Team', validators=[InputRequired()])
display_name = StringField('Display Name', validators=[InputRequired()])
league = SelectField('League', validators=[InputRequired()], choices=[
('', 'Select League'),
('Premier Division', 'Premier Division'),
('1st Division', '1st Division'),
('2nd Division', '2nd Division'),
('3rd Division', '3rd Division'),
('4th Division', '4th Division'),
('5th Division', '5th Division'),
('6th Division', '6th Division')
], coerce=str)
# Action buttons
save_team = SubmitField('Save Team')
cancel = SubmitField('Cancel')
class DataImportForm(FlaskForm):
"""Form for importing data from Hong Kong Hockey Association."""
import_clubs = BooleanField('Import Clubs', default=True)
import_teams = BooleanField('Import Teams', default=True)
import_players = BooleanField('Import Sample Players', default=False)
# Action buttons
import_data = SubmitField('Import Data')
cancel = SubmitField('Cancel')
class ClubSelectionForm(FlaskForm):
"""Form for selecting which clubs to import."""
# This will be populated dynamically with club checkboxes
selected_clubs = FieldList(BooleanField('Select Club'), min_entries=0)
# Action buttons
import_selected = SubmitField('Import Selected Clubs')
select_all = SubmitField('Select All')
select_none = SubmitField('Select None')
cancel = SubmitField('Cancel')
class S3ConfigForm(FlaskForm):
"""Form for S3 configuration."""
# Enable/disable S3
enable_s3 = BooleanField('Enable S3 Storage', default=False)
# Storage provider selection
storage_provider = SelectField('Storage Provider',
choices=[('aws', 'AWS S3'), ('minio', 'MinIO')],
default='aws')
# AWS credentials
aws_access_key_id = StringField('Access Key ID')
aws_secret_access_key = PasswordField('Secret Access Key')
aws_region = StringField('Region', default='us-east-1')
# MinIO specific configuration
minio_endpoint = StringField('MinIO Endpoint',
render_kw={'placeholder': 'minio.example.com:9000'})
minio_use_ssl = BooleanField('Use SSL for MinIO', default=True)
# S3 bucket configuration
bucket_name = StringField('Bucket Name')
bucket_prefix = StringField('Bucket Prefix', default='assets/')
# URL configuration
use_signed_urls = BooleanField('Use Signed URLs', default=True)
signed_url_expiry = IntegerField('Signed URL Expiry (seconds)', default=3600)
fallback_to_static = BooleanField('Fallback to Static Files', default=True)
# Action buttons
test_connection = SubmitField('Test Connection')
save_config = SubmitField('Save Configuration')
cancel = SubmitField('Cancel')

View File

@ -1,13 +0,0 @@
-- Database initialization script for MOTM application
-- This script runs when the PostgreSQL container starts for the first time
-- Create the database if it doesn't exist (this is handled by POSTGRES_DB env var)
-- But we can add any additional setup here
-- Create extensions if needed
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- The application will handle table creation through SQLAlchemy
-- This file is here for any additional database setup that might be needed

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +0,0 @@
# encoding=utf-8
import pymysql
import os
from db_config import sql_read_static
from sqlalchemy import text
def mySettings(setting):
try:
# Convert setting to lowercase for PostgreSQL compatibility
setting_lower = setting.lower()
sql = text("SELECT " + setting_lower + " FROM motmadminsettings WHERE userid='admin'")
rows = sql_read_static(sql)
if rows:
return rows[0][setting_lower]
else:
return None
except Exception as e:
print(e)
return None

View File

@ -1,31 +0,0 @@
Flask>=2.0.0,<3.0.0
Werkzeug>=2.0.0
email-validator
flask_login
Flask-BasicAuth
Flask-Bootstrap
flask_wtf
wtforms>=3.0.0
wtforms_components
MarkupSafe>=2.0.0
# Web scraping
requests>=2.31.0
beautifulsoup4>=4.12.0
lxml>=4.9.0
# SQLAlchemy and database drivers
SQLAlchemy>=2.0.0
Flask-SQLAlchemy>=3.0.0
alembic>=1.12.0
# Database drivers
pymysql>=1.1.0
psycopg2-binary>=2.9.0
PyMySQL>=1.1.0
# AWS S3 support
boto3>=1.34.0
# Legacy support (can be removed after migration)
flask-mysql

View File

@ -1,16 +0,0 @@
@echo off
echo 🐍 Starting MOTM Application...
REM Set PostgreSQL environment variables
set DATABASE_TYPE=postgresql
set POSTGRES_HOST=icarus.ipa.champion
set POSTGRES_PORT=5432
set POSTGRES_DATABASE=motm
set POSTGRES_USER=motm_user
set POSTGRES_PASSWORD=q7y7f7Lv*sODJZ2wGiv0Wq5a
echo 📊 Using PostgreSQL database: %POSTGRES_DATABASE% on %POSTGRES_HOST%
call venv\Scripts\activate.bat
python.exe main.py
pause

View File

@ -1,16 +0,0 @@
#!/bin/bash
echo "🐍 Starting MOTM Application..."
# Set PostgreSQL environment variables
export DATABASE_TYPE=postgresql
export POSTGRES_HOST=icarus.ipa.champion
export POSTGRES_PORT=5432
export POSTGRES_DATABASE=motm
export POSTGRES_USER=motm_user
export POSTGRES_PASSWORD='q7y7f7Lv*sODJZ2wGiv0Wq5a'
echo "📊 Using PostgreSQL database: $POSTGRES_DATABASE on $POSTGRES_HOST"
source venv/bin/activate
python main.py

View File

@ -1,14 +0,0 @@
{
"enable_s3": true,
"storage_provider": "minio",
"aws_access_key_id": "5MoE0Vz8F9vVgulClesUV3GReh2nIiXG",
"aws_secret_access_key": "0h[c8lSHUE'<",
"aws_region": "us-east-1",
"minio_endpoint": "s3.ervine.cloud:443",
"minio_use_ssl": true,
"bucket_name": "hockey-app",
"bucket_prefix": "assets/",
"use_signed_urls": true,
"signed_url_expiry": 3600,
"fallback_to_static": true
}

View File

@ -1,14 +0,0 @@
{
"enable_s3": true,
"storage_provider": "minio",
"aws_access_key_id": "AKIARLJ7D6ZPRRLQHWD7",
"aws_secret_access_key": "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",
"aws_region": "us-east-1",
"minio_endpoint": "s3.ervine.cloud:443",
"minio_use_ssl": true,
"bucket_name": "hockey-app",
"bucket_prefix": "assets/",
"use_signed_urls": true,
"signed_url_expiry": 3600,
"fallback_to_static": true
}

View File

@ -1,390 +0,0 @@
# encoding=utf-8
"""
S3 Configuration and Service Module
Handles S3 credentials and asset retrieval for the MOTM application.
"""
import os
import json
import boto3
from botocore.exceptions import ClientError, NoCredentialsError
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
class S3ConfigManager:
"""Manages S3 configuration settings."""
def __init__(self):
self.config_file = 's3_config.json'
self.config = self.load_config()
def load_config(self) -> Dict[str, Any]:
"""Load S3 configuration from file."""
if os.path.exists(self.config_file):
try:
with open(self.config_file, 'r') as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return self.get_default_config()
return self.get_default_config()
def get_default_config(self) -> Dict[str, Any]:
"""Get default S3 configuration."""
return {
'aws_access_key_id': '',
'aws_secret_access_key': '',
'aws_region': 'us-east-1',
'bucket_name': '',
'bucket_prefix': 'assets/',
'enable_s3': False,
'use_signed_urls': True,
'signed_url_expiry': 3600, # 1 hour in seconds
'fallback_to_static': True,
'storage_provider': 'aws', # 'aws' or 'minio'
'minio_endpoint': '', # MinIO endpoint URL
'minio_use_ssl': True # Whether to use SSL for MinIO
}
def save_config(self, config_data: Dict[str, Any]) -> bool:
"""Save S3 configuration to file."""
try:
# Update environment variables
os.environ['AWS_ACCESS_KEY_ID'] = config_data.get('aws_access_key_id', '')
os.environ['AWS_SECRET_ACCESS_KEY'] = config_data.get('aws_secret_access_key', '')
os.environ['AWS_DEFAULT_REGION'] = config_data.get('aws_region', 'us-east-1')
with open(self.config_file, 'w') as f:
json.dump(config_data, f, indent=2)
self.config = config_data
return True
except IOError:
return False
def get_config_dict(self) -> Dict[str, Any]:
"""Get current configuration as dictionary."""
return self.config.copy()
def test_connection(self, config_data: Optional[Dict[str, Any]] = None) -> tuple[bool, str]:
"""Test S3 connection with provided or current configuration."""
if config_data:
test_config = config_data
else:
test_config = self.config
if not test_config.get('enable_s3', False):
return True, "S3 is disabled - using local static files"
try:
# Determine storage provider
storage_provider = test_config.get('storage_provider', 'aws')
if storage_provider == 'minio':
# Create MinIO client
minio_endpoint = test_config.get('minio_endpoint', '')
if not minio_endpoint:
return False, "MinIO endpoint is required when using MinIO"
# Parse endpoint to remove protocol if present
if minio_endpoint.startswith('http://'):
endpoint_host = minio_endpoint[7:]
use_ssl = False
elif minio_endpoint.startswith('https://'):
endpoint_host = minio_endpoint[8:]
use_ssl = True
else:
endpoint_host = minio_endpoint
use_ssl = test_config.get('minio_use_ssl', True)
# Construct the full endpoint URL
endpoint_url = f"{'https' if use_ssl else 'http'}://{endpoint_host}"
s3_client = boto3.client(
's3',
aws_access_key_id=test_config.get('aws_access_key_id', ''),
aws_secret_access_key=test_config.get('aws_secret_access_key', ''),
region_name=test_config.get('aws_region', 'us-east-1'),
endpoint_url=endpoint_url,
use_ssl=use_ssl,
verify=True # Enable SSL certificate verification
)
provider_name = "MinIO"
else:
# Create AWS S3 client
s3_client = boto3.client(
's3',
aws_access_key_id=test_config.get('aws_access_key_id', ''),
aws_secret_access_key=test_config.get('aws_secret_access_key', ''),
region_name=test_config.get('aws_region', 'us-east-1')
)
provider_name = "AWS S3"
# Test connection by listing bucket
bucket_name = test_config.get('bucket_name', '')
if not bucket_name:
return False, "Bucket name is required when S3 is enabled"
s3_client.head_bucket(Bucket=bucket_name)
return True, f"Successfully connected to {provider_name} bucket: {bucket_name}"
except ClientError as e:
error_code = e.response['Error']['Code']
provider_name = "MinIO" if test_config.get('storage_provider', 'aws') == 'minio' else "AWS S3"
if error_code == '404':
return False, f"{provider_name} bucket '{bucket_name}' not found"
elif error_code == '403':
return False, f"Access denied to {provider_name} bucket. Check credentials and permissions"
else:
return False, f"{provider_name} error: {e.response['Error']['Message']}"
except NoCredentialsError:
return False, "AWS credentials not found"
except Exception as e:
provider_name = "MinIO" if test_config.get('storage_provider', 'aws') == 'minio' else "AWS S3"
return False, f"{provider_name} connection error: {str(e)}"
class S3AssetService:
"""Service for retrieving assets from S3."""
def __init__(self, config_manager: S3ConfigManager):
self.config_manager = config_manager
self._s3_client = None
def _get_public_url(self, bucket_name: str, s3_key: str) -> str:
"""Generate public URL for S3 or MinIO."""
config = self.config_manager.config
storage_provider = config.get('storage_provider', 'aws')
if storage_provider == 'minio':
minio_endpoint = config.get('minio_endpoint', '')
use_ssl = config.get('minio_use_ssl', True)
protocol = 'https' if use_ssl else 'http'
# Parse endpoint to remove protocol if present
if minio_endpoint.startswith('http://'):
minio_endpoint = minio_endpoint[7:]
elif minio_endpoint.startswith('https://'):
minio_endpoint = minio_endpoint[8:]
return f"{protocol}://{minio_endpoint}/{bucket_name}/{s3_key}"
else:
return f"https://{bucket_name}.s3.{config.get('aws_region', 'us-east-1')}.amazonaws.com/{s3_key}"
@property
def s3_client(self):
"""Lazy-loaded S3 client."""
if self._s3_client is None and self.config_manager.config.get('enable_s3', False):
config = self.config_manager.config
storage_provider = config.get('storage_provider', 'aws')
if storage_provider == 'minio':
# Create MinIO client
minio_endpoint = config.get('minio_endpoint', '')
use_ssl = config.get('minio_use_ssl', True)
# Parse endpoint to remove protocol if present
if minio_endpoint.startswith('http://'):
endpoint_host = minio_endpoint[7:]
use_ssl = False
elif minio_endpoint.startswith('https://'):
endpoint_host = minio_endpoint[8:]
use_ssl = True
else:
endpoint_host = minio_endpoint
# Construct the full endpoint URL
endpoint_url = f"{'https' if use_ssl else 'http'}://{endpoint_host}"
self._s3_client = boto3.client(
's3',
aws_access_key_id=config.get('aws_access_key_id', ''),
aws_secret_access_key=config.get('aws_secret_access_key', ''),
region_name=config.get('aws_region', 'us-east-1'),
endpoint_url=endpoint_url,
use_ssl=use_ssl,
verify=True # Enable SSL certificate verification
)
else:
# Create AWS S3 client
self._s3_client = boto3.client(
's3',
aws_access_key_id=config.get('aws_access_key_id', ''),
aws_secret_access_key=config.get('aws_secret_access_key', ''),
region_name=config.get('aws_region', 'us-east-1')
)
return self._s3_client
def get_logo_url(self, logo_path: str, club_name: str = '') -> str:
"""Get logo URL from S3 or fallback to static."""
config = self.config_manager.config
if not config.get('enable_s3', False):
return self._get_static_logo_url(logo_path)
bucket_name = config.get('bucket_name', '')
bucket_prefix = config.get('bucket_prefix', 'motm-assets/')
if not bucket_name:
return self._get_static_logo_url(logo_path)
# Clean up the logo path
if logo_path.startswith('/static/'):
s3_key = bucket_prefix + logo_path.replace('/static/', '')
elif logo_path.startswith('logos/'):
s3_key = bucket_prefix + logo_path
else:
s3_key = bucket_prefix + 'logos/' + logo_path
try:
if config.get('use_signed_urls', True):
# Generate signed URL
expiry = config.get('signed_url_expiry', 3600)
signed_url = self.s3_client.generate_presigned_url(
'get_object',
Params={'Bucket': bucket_name, 'Key': s3_key},
ExpiresIn=expiry
)
return signed_url
else:
# Return public URL
return self._get_public_url(bucket_name, s3_key)
except ClientError:
# Fallback to static if S3 fails
if config.get('fallback_to_static', True):
return self._get_static_logo_url(logo_path)
return ''
def get_logo_url_public(self, logo_path: str, club_name: str = '') -> str:
"""Get logo URL from S3 using public URLs (no authentication)."""
config = self.config_manager.config
if not config.get('enable_s3', False):
return self._get_static_logo_url(logo_path)
bucket_name = config.get('bucket_name', '')
bucket_prefix = config.get('bucket_prefix', 'assets/')
if not bucket_name:
return self._get_static_logo_url(logo_path)
# Clean up the logo path
if logo_path.startswith('/static/'):
s3_key = bucket_prefix + logo_path.replace('/static/', '')
elif logo_path.startswith('logos/'):
s3_key = bucket_prefix + logo_path
else:
s3_key = bucket_prefix + 'logos/' + logo_path
try:
# Always return public URL (no authentication)
return self._get_public_url(bucket_name, s3_key)
except Exception:
# Fallback to static if S3 fails
if config.get('fallback_to_static', True):
return self._get_static_logo_url(logo_path)
return ''
def get_asset_url(self, asset_path: str) -> str:
"""Get any asset URL from S3 or fallback to static."""
config = self.config_manager.config
if not config.get('enable_s3', False):
return f"/static/{asset_path}"
bucket_name = config.get('bucket_name', '')
bucket_prefix = config.get('bucket_prefix', 'motm-assets/')
if not bucket_name:
return f"/static/{asset_path}"
# Clean up the asset path
if asset_path.startswith('/static/'):
s3_key = bucket_prefix + asset_path.replace('/static/', '')
else:
s3_key = bucket_prefix + asset_path
try:
if config.get('use_signed_urls', True):
# Generate signed URL
expiry = config.get('signed_url_expiry', 3600)
signed_url = self.s3_client.generate_presigned_url(
'get_object',
Params={'Bucket': bucket_name, 'Key': s3_key},
ExpiresIn=expiry
)
return signed_url
else:
# Return public URL
return self._get_public_url(bucket_name, s3_key)
except ClientError:
# Fallback to static if S3 fails
if config.get('fallback_to_static', True):
return f"/static/{asset_path}"
return ''
def get_asset_url_public(self, asset_path: str) -> str:
"""Get any asset URL from S3 using public URLs (no authentication)."""
config = self.config_manager.config
if not config.get('enable_s3', False):
return f"/static/{asset_path}"
bucket_name = config.get('bucket_name', '')
bucket_prefix = config.get('bucket_prefix', 'assets/')
if not bucket_name:
return f"/static/{asset_path}"
# Clean up the asset path
if asset_path.startswith('/static/'):
s3_key = bucket_prefix + asset_path.replace('/static/', '')
else:
s3_key = bucket_prefix + asset_path
try:
# Always return public URL (no authentication)
return self._get_public_url(bucket_name, s3_key)
except Exception:
# Fallback to static if S3 fails
if config.get('fallback_to_static', True):
return f"/static/{asset_path}"
return ''
def upload_asset(self, file_path: str, s3_key: str) -> tuple[bool, str]:
"""Upload an asset to S3."""
config = self.config_manager.config
if not config.get('enable_s3', False):
return False, "S3 is disabled"
bucket_name = config.get('bucket_name', '')
bucket_prefix = config.get('bucket_prefix', 'motm-assets/')
if not bucket_name:
return False, "Bucket name not configured"
full_s3_key = bucket_prefix + s3_key
try:
self.s3_client.upload_file(file_path, bucket_name, full_s3_key)
return True, f"Successfully uploaded {s3_key} to S3"
except ClientError as e:
return False, f"Upload failed: {e.response['Error']['Message']}"
except Exception as e:
return False, f"Upload error: {str(e)}"
def _get_static_logo_url(self, logo_path: str) -> str:
"""Get static logo URL as fallback."""
if logo_path.startswith('/static/'):
return logo_path
return f"/static/images/{logo_path}"
# Global instances
s3_config_manager = S3ConfigManager()
s3_asset_service = S3AssetService(s3_config_manager)

View File

@ -1,148 +0,0 @@
# MOTM Flask Application - Virtual Environment Setup (PowerShell)
# Run this script in PowerShell: .\setup_venv.ps1
Write-Host "MOTM Flask Application - Virtual Environment Setup" -ForegroundColor Green
Write-Host "=" * 60 -ForegroundColor Green
# Check Python installation
Write-Host "`n🐍 Checking Python installation..." -ForegroundColor Yellow
$pythonCommands = @('python', 'python3', 'py')
$pythonCmd = $null
foreach ($cmd in $pythonCommands) {
try {
$version = & $cmd --version 2>$null
if ($LASTEXITCODE -eq 0) {
$pythonCmd = $cmd
Write-Host "✅ Found Python: $version" -ForegroundColor Green
break
}
}
catch {
# Continue to next command
}
}
if (-not $pythonCmd) {
Write-Host "❌ Python not found. Please install Python 3.7+ from python.org" -ForegroundColor Red
Write-Host " Make sure to check 'Add Python to PATH' during installation" -ForegroundColor Yellow
Read-Host "Press Enter to exit"
exit 1
}
# Check if virtual environment already exists
$venvDir = "venv"
if (Test-Path $venvDir) {
Write-Host "`n⚠ Virtual environment '$venvDir' already exists" -ForegroundColor Yellow
$recreate = Read-Host "Do you want to recreate it? (y/N)"
if ($recreate -match '^[yY]') {
Write-Host "🗑️ Removing existing virtual environment..." -ForegroundColor Yellow
Remove-Item -Recurse -Force $venvDir
} else {
Write-Host "Using existing virtual environment..." -ForegroundColor Green
$skipCreation = $true
}
}
# Create virtual environment
if (-not $skipCreation) {
Write-Host "`n📦 Creating virtual environment in '$venvDir'..." -ForegroundColor Yellow
try {
& $pythonCmd -m venv $venvDir
if ($LASTEXITCODE -eq 0) {
Write-Host "✅ Virtual environment created successfully!" -ForegroundColor Green
} else {
throw "Virtual environment creation failed"
}
}
catch {
Write-Host "❌ Failed to create virtual environment: $_" -ForegroundColor Red
Read-Host "Press Enter to exit"
exit 1
}
}
# Install dependencies
Write-Host "`n📚 Installing dependencies..." -ForegroundColor Yellow
$pipCmd = Join-Path $venvDir "Scripts\pip.exe"
if (-not (Test-Path $pipCmd)) {
$pipCmd = Join-Path $venvDir "Scripts\pip"
}
try {
# Upgrade pip first
Write-Host "🔄 Upgrading pip..." -ForegroundColor Yellow
& $pipCmd install --upgrade pip
# Install requirements
Write-Host "📦 Installing application dependencies..." -ForegroundColor Yellow
& $pipCmd install -r requirements.txt
if ($LASTEXITCODE -eq 0) {
Write-Host "✅ Dependencies installed successfully!" -ForegroundColor Green
} else {
throw "Dependencies installation failed"
}
}
catch {
Write-Host "❌ Failed to install dependencies: $_" -ForegroundColor Red
Read-Host "Press Enter to exit"
exit 1
}
# Create convenience scripts
Write-Host "`n📝 Creating convenience scripts..." -ForegroundColor Yellow
# Update the batch files to use the correct Python command
$activateScript = @"
@echo off
echo 🐍 Activating MOTM Virtual Environment...
call venv\Scripts\activate.bat
echo Virtual environment activated!
echo.
echo 🚀 To start the MOTM application, run:
echo $pythonCmd main.py
echo.
echo 🔧 To deactivate, run:
echo deactivate
"@
$runScript = @"
@echo off
echo 🐍 Starting MOTM Application...
call venv\Scripts\activate.bat
$pythonCmd main.py
pause
"@
$activateScript | Out-File -FilePath "activate_motm.bat" -Encoding ASCII
$runScript | Out-File -FilePath "run_motm.bat" -Encoding ASCII
Write-Host "✅ Convenience scripts created!" -ForegroundColor Green
# Print completion message
Write-Host "`n" + "=" * 60 -ForegroundColor Green
Write-Host "🎉 MOTM Virtual Environment Setup Complete!" -ForegroundColor Green
Write-Host "=" * 60 -ForegroundColor Green
Write-Host "`n📋 To use the virtual environment:" -ForegroundColor Cyan
Write-Host " 1. Activate: .\activate_motm.bat" -ForegroundColor White
Write-Host " 2. Run app: .\run_motm.bat" -ForegroundColor White
Write-Host " 3. Deactivate: deactivate" -ForegroundColor White
Write-Host "`n🔧 Manual activation:" -ForegroundColor Cyan
Write-Host " venv\Scripts\activate.bat" -ForegroundColor White
Write-Host " $pythonCmd main.py" -ForegroundColor White
Write-Host "`n🌐 The application will be available at: http://localhost:5000" -ForegroundColor Cyan
Write-Host "`n📚 For development:" -ForegroundColor Cyan
Write-Host " - Activate the venv before installing new packages" -ForegroundColor White
Write-Host " - Use 'pip install <package>' to add dependencies" -ForegroundColor White
Write-Host " - Update requirements.txt with 'pip freeze > requirements.txt'" -ForegroundColor White
Write-Host "`n🚀 Ready to start! Run: .\run_motm.bat" -ForegroundColor Green
Read-Host "`nPress Enter to exit"

View File

@ -1,224 +0,0 @@
#!/usr/bin/env python3
"""
Setup script to create and configure a Python virtual environment for the MOTM application.
"""
import os
import sys
import subprocess
import platform
def create_virtual_environment():
"""Create a Python virtual environment for the MOTM application."""
print("🐍 Setting up Python virtual environment for MOTM application...")
print("=" * 60)
# Check Python version
python_version = sys.version_info
print(f"✓ Python version: {python_version.major}.{python_version.minor}.{python_version.micro}")
if python_version < (3, 7):
print("❌ Python 3.7 or higher is required")
return False
# Determine the virtual environment directory name
venv_dir = "venv"
# Check if virtual environment already exists
if os.path.exists(venv_dir):
print(f"⚠ Virtual environment '{venv_dir}' already exists")
response = input("Do you want to recreate it? (y/N): ").strip().lower()
if response in ['y', 'yes']:
print(f"🗑️ Removing existing virtual environment...")
if platform.system() == "Windows":
subprocess.run(['rmdir', '/s', '/q', venv_dir], shell=True)
else:
subprocess.run(['rm', '-rf', venv_dir])
else:
print("Using existing virtual environment...")
return True
# Create virtual environment
print(f"📦 Creating virtual environment in '{venv_dir}'...")
try:
subprocess.run([sys.executable, '-m', 'venv', venv_dir], check=True)
print("✅ Virtual environment created successfully!")
except subprocess.CalledProcessError as e:
print(f"❌ Failed to create virtual environment: {e}")
return False
return True
def get_activation_script():
"""Get the appropriate activation script for the current platform."""
if platform.system() == "Windows":
return os.path.join("venv", "Scripts", "activate.bat")
else:
return os.path.join("venv", "bin", "activate")
def install_dependencies():
"""Install required dependencies in the virtual environment."""
print("\n📚 Installing dependencies...")
# Determine the pip command based on platform
if platform.system() == "Windows":
pip_cmd = os.path.join("venv", "Scripts", "pip")
else:
pip_cmd = os.path.join("venv", "bin", "pip")
# Check if requirements.txt exists
if not os.path.exists("requirements.txt"):
print("❌ requirements.txt not found")
return False
try:
# Upgrade pip first
print("🔄 Upgrading pip...")
subprocess.run([pip_cmd, "install", "--upgrade", "pip"], check=True)
# Install requirements
print("📦 Installing application dependencies...")
subprocess.run([pip_cmd, "install", "-r", "requirements.txt"], check=True)
print("✅ Dependencies installed successfully!")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Failed to install dependencies: {e}")
return False
def create_activation_scripts():
"""Create convenient activation scripts."""
print("\n📝 Creating activation scripts...")
# Windows batch script
windows_script = """@echo off
echo 🐍 Activating MOTM Virtual Environment...
call venv\\Scripts\\activate.bat
echo Virtual environment activated!
echo.
echo 🚀 To start the MOTM application, run:
echo python main.py
echo.
echo 🔧 To deactivate, run:
echo deactivate
"""
# Unix shell script
unix_script = """#!/bin/bash
echo "🐍 Activating MOTM Virtual Environment..."
source venv/bin/activate
echo "✅ Virtual environment activated!"
echo ""
echo "🚀 To start the MOTM application, run:"
echo " python main.py"
echo ""
echo "🔧 To deactivate, run:"
echo " deactivate"
"""
# Write platform-specific scripts
if platform.system() == "Windows":
with open("activate_motm.bat", "w") as f:
f.write(windows_script)
print("✓ Created activate_motm.bat for Windows")
else:
with open("activate_motm.sh", "w") as f:
f.write(unix_script)
# Make it executable
os.chmod("activate_motm.sh", 0o755)
print("✓ Created activate_motm.sh for Unix/Linux")
return True
def create_run_script():
"""Create a script to run the application directly."""
print("📝 Creating run script...")
# Windows batch script
windows_run = """@echo off
echo 🐍 Starting MOTM Application...
call venv\\Scripts\\activate.bat
python main.py
pause
"""
# Unix shell script
unix_run = """#!/bin/bash
echo "🐍 Starting MOTM Application..."
source venv/bin/activate
python main.py
"""
if platform.system() == "Windows":
with open("run_motm.bat", "w") as f:
f.write(windows_run)
print("✓ Created run_motm.bat")
else:
with open("run_motm.sh", "w") as f:
f.write(unix_run)
os.chmod("run_motm.sh", 0o755)
print("✓ Created run_motm.sh")
def print_instructions():
"""Print setup completion instructions."""
print("\n" + "=" * 60)
print("🎉 MOTM Virtual Environment Setup Complete!")
print("=" * 60)
if platform.system() == "Windows":
print("\n📋 To use the virtual environment:")
print(" 1. Activate: activate_motm.bat")
print(" 2. Run app: run_motm.bat")
print(" 3. Deactivate: deactivate")
print("\n🔧 Manual activation:")
print(" venv\\Scripts\\activate.bat")
print(" python main.py")
else:
print("\n📋 To use the virtual environment:")
print(" 1. Activate: source activate_motm.sh")
print(" 2. Run app: ./run_motm.sh")
print(" 3. Deactivate: deactivate")
print("\n🔧 Manual activation:")
print(" source venv/bin/activate")
print(" python main.py")
print("\n🌐 The application will be available at: http://localhost:5000")
print("\n📚 For development:")
print(" - Activate the venv before installing new packages")
print(" - Use 'pip install <package>' to add dependencies")
print(" - Update requirements.txt with 'pip freeze > requirements.txt'")
def main():
"""Main setup function."""
# Change to the script's directory
script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir)
print("MOTM Flask Application - Virtual Environment Setup")
print("=" * 60)
# Step 1: Create virtual environment
if not create_virtual_environment():
sys.exit(1)
# Step 2: Install dependencies
if not install_dependencies():
sys.exit(1)
# Step 3: Create convenience scripts
create_activation_scripts()
create_run_script()
# Step 4: Print instructions
print_instructions()
if __name__ == "__main__":
main()

View File

@ -1,71 +0,0 @@
@echo off
echo MOTM Flask Application - Virtual Environment Setup
echo ================================================
echo.
echo 🐍 Creating Python virtual environment...
REM Check if virtual environment already exists
if exist venv (
echo ⚠ Virtual environment 'venv' already exists
set /p recreate="Do you want to recreate it? (y/N): "
if /i "%recreate%"=="y" (
echo 🗑️ Removing existing virtual environment...
rmdir /s /q venv
) else (
echo Using existing virtual environment...
goto :install_deps
)
)
REM Create virtual environment
echo 📦 Creating virtual environment in 'venv'...
python.exe -m venv venv
if errorlevel 1 (
echo ❌ Failed to create virtual environment
pause
exit /b 1
)
echo ✅ Virtual environment created successfully!
:install_deps
echo.
echo 📚 Installing dependencies...
REM Upgrade pip first
echo 🔄 Upgrading pip...
venv\Scripts\pip install --upgrade pip
REM Install requirements
echo 📦 Installing application dependencies...
venv\Scripts\pip install -r requirements.txt
if errorlevel 1 (
echo ❌ Failed to install dependencies
pause
exit /b 1
)
echo ✅ Dependencies installed successfully!
echo.
echo ================================================
echo 🎉 MOTM Virtual Environment Setup Complete!
echo ================================================
echo.
echo 📋 To use the virtual environment:
echo 1. Activate: activate_motm.bat
echo 2. Run app: run_motm.bat
echo 3. Deactivate: deactivate
echo.
echo 🔧 Manual activation:
echo venv\Scripts\activate.bat
echo python main.py
echo.
echo 🌐 The application will be available at: http://localhost:5000
echo.
echo 📚 For development:
echo - Activate the venv before installing new packages
echo - Use 'pip install ^<package^>' to add dependencies
echo - Update requirements.txt with 'pip freeze ^> requirements.txt'
echo.
pause

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +0,0 @@
<svg width="120" height="80" xmlns="http://www.w3.org/2000/svg">
<rect width="120" height="80" fill="#f8f9fa" stroke="#dee2e6" stroke-width="2"/>
<text x="60" y="45" font-family="Arial, sans-serif" font-size="12" text-anchor="middle" fill="#6c757d">Club Logo</text>
</svg>

Before

Width:  |  Height:  |  Size: 277 B

View File

@ -1 +0,0 @@
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==

File diff suppressed because one or more lines are too long

View File

@ -1,44 +0,0 @@
from markupsafe import Markup
class matchSquadTable:
def __init__(self, items):
self.items = items
self.border = True
self.classes = []
def __html__(self):
"""Generate HTML table from items"""
if not self.items:
return Markup('<p>No players in squad</p>')
# Start table
classes_str = ' '.join(self.classes) if self.classes else ''
border_attr = 'border="1"' if self.border else ''
html = f'<table class="table {classes_str}" {border_attr}>\n'
# Table header
html += ' <thead>\n <tr>\n'
html += ' <th>Player Number</th>\n'
html += ' <th>Nickname</th>\n'
html += ' <th>Surname</th>\n'
html += ' <th>Forenames</th>\n'
html += ' <th>Delete</th>\n'
html += ' </tr>\n </thead>\n'
# Table body
html += ' <tbody>\n'
for item in self.items:
html += ' <tr>\n'
html += f' <td>{item.get("playernumber", "")}</td>\n'
html += f' <td>{item.get("playernickname", "")}</td>\n'
html += f' <td>{item.get("playersurname", "")}</td>\n'
html += f' <td>{item.get("playerforenames", "")}</td>\n'
html += f' <td><form method="post" action="/admin/squad/remove?playerNumber={item.get("playernumber", "")}"><button type="submit" class="btn btn-danger">Delete</button></form></td>\n'
html += ' </tr>\n'
html += ' </tbody>\n'
# End table
html += '</table>\n'
return Markup(html)

View File

@ -1,334 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add Club - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3>Add New Club</h3>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.hockey_club.label(class="form-label") }}
{{ form.hockey_club(class="form-control") }}
{% if form.hockey_club.errors %}
<div class="text-danger">
{% for error in form.hockey_club.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.logo_url.label(class="form-label") }}
<div class="input-group">
{{ form.logo_url(class="form-control", id="logoUrl", onchange="previewLogo()") }}
<button type="button" class="btn btn-outline-secondary" onclick="browseS3()" title="Browse S3 Storage">
<i class="fas fa-cloud"></i> Browse S3
</button>
</div>
{% if form.logo_url.errors %}
<div class="text-danger">
{% for error in form.logo_url.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">Enter the full URL to the club's logo image or use the S3 browser to select from your configured storage.</small>
<!-- Logo Preview -->
<div id="logoPreview" class="mt-2" style="display: none;">
<label class="form-label small">Preview:</label>
<div class="border rounded p-2 bg-light">
<img id="previewImage" src="" alt="Logo preview" style="max-height: 80px; max-width: 120px;" onerror="this.style.display='none'; document.getElementById('previewError').style.display='block';" onload="document.getElementById('previewError').style.display='none';">
<div id="previewError" class="text-muted small" style="display: none;">Unable to load image</div>
</div>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
{{ form.cancel(class="btn btn-secondary me-md-2") }}
{{ form.save_club(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
<!-- S3 Browser Modal -->
<div class="modal fade" id="s3BrowserModal" tabindex="-1" aria-labelledby="s3BrowserModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="s3BrowserModalLabel">Browse S3 Storage</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="s3BrowserContent">
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p>Loading S3 contents...</p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="selectS3FileBtn" onclick="selectS3File()" disabled>Select File</button>
</div>
</div>
</div>
</div>
<div class="mt-3">
<a href="/admin/clubs" class="btn btn-outline-secondary">Back to Club Management</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
function previewLogo() {
const urlInput = document.getElementById('logoUrl');
const previewDiv = document.getElementById('logoPreview');
const previewImg = document.getElementById('previewImage');
if (urlInput.value.trim()) {
previewImg.src = urlInput.value;
previewDiv.style.display = 'block';
} else {
previewDiv.style.display = 'none';
}
}
// S3 Browser functionality
let selectedS3File = null;
let currentS3Path = '';
function browseS3() {
// Reset state
selectedS3File = null;
currentS3Path = '';
// Show loading state
document.getElementById('s3BrowserContent').innerHTML = `
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p>Loading S3 contents...</p>
</div>
`;
// Show modal
const s3BrowserModal = new bootstrap.Modal(document.getElementById('s3BrowserModal'));
s3BrowserModal.show();
// Load S3 contents from root of assets folder
loadS3Contents('');
}
function loadS3Contents(path) {
fetch(`/admin/api/s3-browser?path=${encodeURIComponent(path)}`)
.then(response => response.json())
.then(data => {
if (data.success) {
displayS3Contents(data);
} else {
document.getElementById('s3BrowserContent').innerHTML = `
<div class="alert alert-danger" role="alert">
<strong>Error:</strong> ${data.message}
</div>
`;
}
})
.catch(error => {
console.error('Error:', error);
document.getElementById('s3BrowserContent').innerHTML = `
<div class="alert alert-danger" role="alert">
<strong>Error:</strong> Failed to load S3 contents. Please try again.
</div>
`;
});
}
function displayS3Contents(data) {
currentS3Path = data.path;
let html = `
<div class="row mb-3">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="#" onclick="loadS3Contents('')">assets</a>
</li>
`;
// Build breadcrumb
if (data.path !== '') {
const pathParts = data.path.split('/').filter(p => p);
let currentPath = '';
pathParts.forEach((part, index) => {
currentPath += part + '/';
const isLast = index === pathParts.length - 1;
html += `<li class="breadcrumb-item ${isLast ? 'active' : ''}">`;
if (!isLast) {
html += `<a href="#" onclick="loadS3Contents('${currentPath}')">${part}</a>`;
} else {
html += part;
}
html += '</li>';
});
}
html += `
</ol>
</nav>
</div>
</div>
<div class="row">
`;
// Display folders
if (data.folders.length > 0) {
data.folders.forEach(folder => {
html += `
<div class="col-md-3 mb-3">
<div class="card h-100 folder-card" onclick="loadS3Contents('${folder.path}')" style="cursor: pointer;">
<div class="card-body text-center">
<i class="fas fa-folder fa-3x text-warning mb-2"></i>
<h6 class="card-title">${folder.name}</h6>
</div>
</div>
</div>
`;
});
}
// Display files
if (data.files.length > 0) {
data.files.forEach(file => {
const isImage = /\.(jpg|jpeg|png|gif|svg|webp)$/i.test(file.name);
const fileSize = formatFileSize(file.size);
html += `
<div class="col-md-3 mb-3">
<div class="card h-100 file-card" onclick="selectS3FileItem('${file.path}', '${file.url}')" style="cursor: pointer;">
<div class="card-body text-center">
`;
if (isImage) {
html += `
<img src="${file.url}" alt="${file.name}" class="img-fluid mb-2" style="max-height: 100px; max-width: 100%; object-fit: contain;">
`;
} else {
html += `
<i class="fas fa-file fa-3x text-primary mb-2"></i>
`;
}
html += `
<h6 class="card-title">${file.name}</h6>
<small class="text-muted">${fileSize}</small>
</div>
</div>
</div>
`;
});
}
if (data.folders.length === 0 && data.files.length === 0) {
html += `
<div class="col-12">
<div class="alert alert-info text-center">
<i class="fas fa-info-circle"></i> No files or folders found in this directory.
</div>
</div>
`;
}
html += `
</div>
`;
document.getElementById('s3BrowserContent').innerHTML = html;
}
function selectS3FileItem(filePath, fileUrl) {
// Remove previous selection
document.querySelectorAll('.file-card').forEach(card => {
card.classList.remove('border-primary');
});
// Add selection to clicked card
event.currentTarget.classList.add('border-primary');
// Store selected file
selectedS3File = {
path: filePath,
url: fileUrl
};
// Enable select button
document.getElementById('selectS3FileBtn').disabled = false;
}
function selectS3File() {
if (selectedS3File) {
// Update the logo URL field
const logoUrlField = document.getElementById('logoUrl');
if (logoUrlField) {
logoUrlField.value = selectedS3File.path;
// Trigger preview update
previewLogo();
}
// Close modal
const s3BrowserModal = bootstrap.Modal.getInstance(document.getElementById('s3BrowserModal'));
s3BrowserModal.hide();
}
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Preview logo on page load if URL is already filled
document.addEventListener('DOMContentLoaded', function() {
previewLogo();
});
</script>
</body>
</html>

View File

@ -1,109 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add Player - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3>Add New Player</h3>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.player_number.label(class="form-label") }}
{{ form.player_number(class="form-control") }}
{% if form.player_number.errors %}
<div class="text-danger">
{% for error in form.player_number.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.player_forenames.label(class="form-label") }}
{{ form.player_forenames(class="form-control") }}
{% if form.player_forenames.errors %}
<div class="text-danger">
{% for error in form.player_forenames.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.player_surname.label(class="form-label") }}
{{ form.player_surname(class="form-control") }}
{% if form.player_surname.errors %}
<div class="text-danger">
{% for error in form.player_surname.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.player_nickname.label(class="form-label") }}
{{ form.player_nickname(class="form-control") }}
{% if form.player_nickname.errors %}
<div class="text-danger">
{% for error in form.player_nickname.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.player_team.label(class="form-label") }}
{{ form.player_team(class="form-select") }}
{% if form.player_team.errors %}
<div class="text-danger">
{% for error in form.player_team.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
{{ form.cancel(class="btn btn-secondary me-md-2") }}
{{ form.save_player(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
<div class="mt-3">
<a href="/admin/players" class="btn btn-outline-secondary">Back to Player Management</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,101 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add Team - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3>Add New Team</h3>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.club.label(class="form-label") }}
{{ form.club(class="form-select") }}
{% if form.club.errors %}
<div class="text-danger">
{% for error in form.club.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">Select the club from the list of available clubs</small>
</div>
<div class="mb-3">
{{ form.team.label(class="form-label") }}
{{ form.team(class="form-control") }}
{% if form.team.errors %}
<div class="text-danger">
{% for error in form.team.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">Enter the team identifier (e.g., A, B, C, 1st, 2nd)</small>
</div>
<div class="mb-3">
{{ form.display_name.label(class="form-label") }}
{{ form.display_name(class="form-control") }}
{% if form.display_name.errors %}
<div class="text-danger">
{% for error in form.display_name.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">Enter the full display name (e.g., HKFC A, KCC 1st Team)</small>
</div>
<div class="mb-3">
{{ form.league.label(class="form-label") }}
{{ form.league(class="form-select") }}
{% if form.league.errors %}
<div class="text-danger">
{% for error in form.league.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">Select the league/division from the list</small>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
{{ form.cancel(class="btn btn-secondary me-md-2") }}
{{ form.save_team(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
<div class="mt-3">
<a href="/admin/teams" class="btn btn-outline-secondary">Back to Team Management</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,219 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Dashboard - HKFC Men's C Team MOTM System</title>
<link rel="stylesheet" media="screen" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
<style>
.admin-section {
margin-bottom: 30px;
}
.section-header {
background-color: #f5f5f5;
padding: 15px;
border-left: 4px solid #337ab7;
margin-bottom: 15px;
}
.card-custom {
transition: transform 0.2s;
}
.card-custom:hover {
transform: translateY(-5px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="page-header">
<h1>HKFC Men's C Team - Admin Dashboard</h1>
<p class="lead">Central hub for all administrative functions</p>
</div>
<div class="mb-3">
<a href="/" class="btn btn-default">Back to Main Page</a>
<a href="/admin/profile" class="btn btn-outline-secondary">Admin Profile</a>
</div>
<!-- Data Management Section -->
<div class="admin-section">
<div class="section-header">
<h3>Data Management</h3>
</div>
<div class="row">
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/players" class="list-group-item">
<h4 class="list-group-item-heading">Player Management</h4>
<p class="list-group-item-text">Add, edit, and manage players</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/clubs" class="list-group-item">
<h4 class="list-group-item-heading">Club Management</h4>
<p class="list-group-item-text">Manage hockey clubs</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/teams" class="list-group-item">
<h4 class="list-group-item-heading">Team Management</h4>
<p class="list-group-item-text">Manage hockey teams</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/import" class="list-group-item">
<h4 class="list-group-item-heading">Data Import</h4>
<p class="list-group-item-text">Import clubs and teams</p>
</a>
</div>
</div>
</div>
</div>
<!-- Match Management Section -->
<div class="admin-section">
<div class="section-header">
<h3>Match Management</h3>
</div>
<div class="row">
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/squad" class="list-group-item">
<h4 class="list-group-item-heading">Squad Selection</h4>
<p class="list-group-item-text">Select match squad</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/squad/list" class="list-group-item">
<h4 class="list-group-item-heading">View Squad</h4>
<p class="list-group-item-text">View current squad</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/squad/reset" class="list-group-item">
<h4 class="list-group-item-heading">Reset Squad</h4>
<p class="list-group-item-text">Reset for new match</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/stats" class="list-group-item">
<h4 class="list-group-item-heading">Goals & Assists</h4>
<p class="list-group-item-text">Record statistics</p>
</a>
</div>
</div>
</div>
</div>
<!-- MOTM Management Section -->
<div class="admin-section">
<div class="section-header">
<h3>MOTM Management</h3>
</div>
<div class="row">
<div class="col-md-4">
<div class="list-group card-custom">
<a href="/admin/motm" class="list-group-item">
<h4 class="list-group-item-heading">MOTM Admin</h4>
<p class="list-group-item-text">Manage match settings and activate voting</p>
</a>
</div>
</div>
<div class="col-md-4">
<div class="list-group card-custom">
<a href="/admin/voting" class="list-group-item">
<h4 class="list-group-item-heading">Voting Results</h4>
<p class="list-group-item-text">View current match results</p>
</a>
</div>
</div>
<div class="col-md-4">
<div class="list-group card-custom">
<a href="/admin/motm/manage" class="list-group-item">
<h4 class="list-group-item-heading">MOTM Management</h4>
<p class="list-group-item-text">Reset MOTM/DotD counts for specific fixtures</p>
</a>
</div>
</div>
<div class="col-md-4">
<div class="list-group card-custom">
<a href="/admin/device-tracking" class="list-group-item">
<h4 class="list-group-item-heading">Device Tracking</h4>
<p class="list-group-item-text">Monitor voting patterns and detect duplicate votes</p>
</a>
</div>
</div>
<div class="col-md-4">
<div class="list-group card-custom">
<a href="/admin/poty" class="list-group-item">
<h4 class="list-group-item-heading">Player of the Year</h4>
<p class="list-group-item-text">View season standings</p>
</a>
</div>
</div>
</div>
</div>
<!-- System Management Section -->
<div class="admin-section">
<div class="section-header">
<h3>System Management</h3>
</div>
<div class="row">
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/database-setup" class="list-group-item">
<h4 class="list-group-item-heading">Database Setup</h4>
<p class="list-group-item-text">Configure and initialize database</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/database-status" class="list-group-item">
<h4 class="list-group-item-heading">Database Status</h4>
<p class="list-group-item-text">View database configuration</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/s3-config" class="list-group-item">
<h4 class="list-group-item-heading">S3 Configuration</h4>
<p class="list-group-item-text">Configure S3/MinIO storage</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/s3-status" class="list-group-item">
<h4 class="list-group-item-heading">S3 Status</h4>
<p class="list-group-item-text">View S3/MinIO status</p>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,160 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Profile - HKFC Men's C Team MOTM System</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.profile-section {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 2rem;
margin-bottom: 2rem;
}
.security-notice {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 2rem;
}
</style>
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-8 mx-auto">
<h1>Admin Profile</h1>
<p class="lead">Manage your admin account settings and password</p>
<div class="mb-3">
<a href="/admin" class="btn btn-outline-primary">Back to Admin Dashboard</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Security Notice -->
<div class="security-notice">
<h5><i class="bi bi-shield-check"></i> Security Notice</h5>
<p class="mb-0">
<strong>Important:</strong> Changing your password will immediately affect access to all admin functions.
Make sure to remember your new password or store it securely.
</p>
</div>
<!-- Profile Form -->
<div class="profile-section">
<h3>Change Password & Settings</h3>
<form method="POST">
{{ form.hidden_tag() }}
<div class="row">
<div class="col-md-6">
<div class="mb-3">
{{ form.current_password.label(class="form-label") }}
{{ form.current_password(class="form-control", placeholder="Enter current password") }}
{% if form.current_password.errors %}
<div class="text-danger">
{% for error in form.current_password.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
{{ form.new_password.label(class="form-label") }}
{{ form.new_password(class="form-control", placeholder="Enter new password (min 6 characters)") }}
{% if form.new_password.errors %}
<div class="text-danger">
{% for error in form.new_password.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
{{ form.confirm_password.label(class="form-label") }}
{{ form.confirm_password(class="form-control", placeholder="Confirm new password") }}
{% if form.confirm_password.errors %}
<div class="text-danger">
{% for error in form.confirm_password.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control", placeholder="admin@example.com") }}
{% if form.email.errors %}
<div class="text-danger">
{% for error in form.email.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary">Update Profile</button>
<a href="/admin" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
<!-- Current Profile Info -->
<div class="profile-section">
<h3>Current Profile Information</h3>
<div class="row">
<div class="col-md-6">
<p><strong>Username:</strong> admin</p>
<p><strong>Email:</strong> {{ current_email or 'Not set' }}</p>
</div>
<div class="col-md-6">
<p><strong>Account Type:</strong> Administrator</p>
<p><strong>Access Level:</strong> Full Admin Rights</p>
</div>
</div>
</div>
<!-- Password Requirements -->
<div class="profile-section">
<h3>Password Requirements</h3>
<ul class="list-unstyled">
<li><i class="bi bi-check-circle text-success"></i> Minimum 6 characters</li>
<li><i class="bi bi-check-circle text-success"></i> Must match confirmation</li>
<li><i class="bi bi-check-circle text-success"></i> Current password must be correct</li>
</ul>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,544 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Club Management - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<h1>Club Management</h1>
<p class="lead">Manage hockey clubs in the database</p>
<div class="mb-3">
<a href="/admin/clubs/add" class="btn btn-primary">Add New Club</a>
<button type="button" class="btn btn-info" id="importClubsBtn" onclick="importClubs()">
<span class="spinner-border spinner-border-sm d-none" id="importSpinner"></span>
Import from Hockey HK
</button>
<button type="button" class="btn btn-outline-info" id="previewClubsBtn" onclick="previewClubs()">
Preview Clubs
</button>
<a href="/admin" class="btn btn-secondary">Back to Admin</a>
</div>
<!-- Import Status -->
<div id="importStatus" class="alert d-none" role="alert"></div>
<!-- Preview Modal -->
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="previewModalLabel">Clubs from Hockey Hong Kong</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="previewContent">
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p>Loading clubs...</p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="confirmImportBtn" onclick="confirmImport()">Import All</button>
</div>
</div>
</div>
</div>
<!-- S3 Browser Modal -->
<div class="modal fade" id="s3BrowserModal" tabindex="-1" aria-labelledby="s3BrowserModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="s3BrowserModalLabel">Browse S3 Storage</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="s3BrowserContent">
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p>Loading S3 contents...</p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="selectS3FileBtn" onclick="selectS3File()" disabled>Select File</button>
</div>
</div>
</div>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="card">
<div class="card-header">
<h5>All Clubs</h5>
</div>
<div class="card-body">
{% if clubs %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>Club Name</th>
<th>Logo</th>
<th>Logo URL</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for club in clubs %}
<tr>
<td>{{ club.id }}</td>
<td>{{ club.hockey_club }}</td>
<td>
{% if club.logo_url %}
<img src="{{ s3_asset_service.get_logo_url(club.logo_url, club.hockey_club) }}" alt="{{ club.hockey_club }} logo" style="max-height: 40px; max-width: 60px;" onerror="this.style.display='none'">
{% else %}
<span class="text-muted">No logo</span>
{% endif %}
</td>
<td>
{% if club.logo_url %}
<a href="{{ club.logo_url }}" target="_blank" class="text-decoration-none small">
{{ club.logo_url[:50] }}{% if club.logo_url|length > 50 %}...{% endif %}
</a>
{% else %}
<span class="text-muted">No URL</span>
{% endif %}
</td>
<td>
<a href="/admin/clubs/edit/{{ club.id }}" class="btn btn-sm btn-outline-primary">Edit</a>
<form method="POST" action="/admin/clubs/delete/{{ club.id }}" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this club?')">
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<h5>No clubs found</h5>
<p>There are no clubs in the database. <a href="/admin/clubs/add">Add the first club</a> to get started.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let previewClubs = [];
function showStatus(message, type = 'info') {
const statusDiv = document.getElementById('importStatus');
statusDiv.className = `alert alert-${type}`;
statusDiv.textContent = message;
statusDiv.classList.remove('d-none');
// Auto-hide after 5 seconds
setTimeout(() => {
statusDiv.classList.add('d-none');
}, 5000);
}
// S3 Browser functionality
let selectedS3File = null;
let currentS3Path = '';
function browseS3() {
// Reset state
selectedS3File = null;
currentS3Path = '';
// Show loading state
document.getElementById('s3BrowserContent').innerHTML = `
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p>Loading S3 contents...</p>
</div>
`;
// Show modal
const s3BrowserModal = new bootstrap.Modal(document.getElementById('s3BrowserModal'));
s3BrowserModal.show();
// Load S3 contents from root of assets folder
loadS3Contents('');
}
function loadS3Contents(path) {
fetch(`/admin/api/s3-browser?path=${encodeURIComponent(path)}`)
.then(response => response.json())
.then(data => {
if (data.success) {
displayS3Contents(data);
} else {
document.getElementById('s3BrowserContent').innerHTML = `
<div class="alert alert-danger" role="alert">
<strong>Error:</strong> ${data.message}
</div>
`;
}
})
.catch(error => {
console.error('Error:', error);
document.getElementById('s3BrowserContent').innerHTML = `
<div class="alert alert-danger" role="alert">
<strong>Error:</strong> Failed to load S3 contents. Please try again.
</div>
`;
});
}
function displayS3Contents(data) {
currentS3Path = data.path;
let html = `
<div class="row mb-3">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="#" onclick="loadS3Contents('')">assets</a>
</li>
`;
// Build breadcrumb
if (data.path !== '') {
const pathParts = data.path.split('/').filter(p => p);
let currentPath = '';
pathParts.forEach((part, index) => {
currentPath += part + '/';
const isLast = index === pathParts.length - 1;
html += `<li class="breadcrumb-item ${isLast ? 'active' : ''}">`;
if (!isLast) {
html += `<a href="#" onclick="loadS3Contents('${currentPath}')">${part}</a>`;
} else {
html += part;
}
html += '</li>';
});
}
html += `
</ol>
</nav>
</div>
</div>
<div class="row">
`;
// Display folders
if (data.folders.length > 0) {
data.folders.forEach(folder => {
html += `
<div class="col-md-3 mb-3">
<div class="card h-100 folder-card" onclick="loadS3Contents('${folder.path}')" style="cursor: pointer;">
<div class="card-body text-center">
<i class="fas fa-folder fa-3x text-warning mb-2"></i>
<h6 class="card-title">${folder.name}</h6>
</div>
</div>
</div>
`;
});
}
// Display files
if (data.files.length > 0) {
data.files.forEach(file => {
const isImage = /\.(jpg|jpeg|png|gif|svg|webp)$/i.test(file.name);
const fileSize = formatFileSize(file.size);
html += `
<div class="col-md-3 mb-3">
<div class="card h-100 file-card" onclick="selectS3FileItem('${file.path}', '${file.url}')" style="cursor: pointer;">
<div class="card-body text-center">
`;
if (isImage) {
html += `
<img src="${file.url}" alt="${file.name}" class="img-fluid mb-2" style="max-height: 100px; max-width: 100%; object-fit: contain;">
`;
} else {
html += `
<i class="fas fa-file fa-3x text-primary mb-2"></i>
`;
}
html += `
<h6 class="card-title">${file.name}</h6>
<small class="text-muted">${fileSize}</small>
</div>
</div>
</div>
`;
});
}
if (data.folders.length === 0 && data.files.length === 0) {
html += `
<div class="col-12">
<div class="alert alert-info text-center">
<i class="fas fa-info-circle"></i> No files or folders found in this directory.
</div>
</div>
`;
}
html += `
</div>
`;
document.getElementById('s3BrowserContent').innerHTML = html;
}
function selectS3FileItem(filePath, fileUrl) {
// Remove previous selection
document.querySelectorAll('.file-card').forEach(card => {
card.classList.remove('border-primary');
});
// Add selection to clicked card
event.currentTarget.classList.add('border-primary');
// Store selected file
selectedS3File = {
path: filePath,
url: fileUrl
};
// Enable select button
document.getElementById('selectS3FileBtn').disabled = false;
}
function selectS3File() {
if (selectedS3File) {
// Update the logo URL field in the add club form
const logoUrlField = document.querySelector('input[name="logo_url"]');
if (logoUrlField) {
logoUrlField.value = selectedS3File.path;
}
// Close modal
const s3BrowserModal = bootstrap.Modal.getInstance(document.getElementById('s3BrowserModal'));
s3BrowserModal.hide();
// Show success message
showStatus('Logo selected successfully!', 'success');
}
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function previewClubs() {
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
const content = document.getElementById('previewContent');
// Show loading state
content.innerHTML = `
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p>Loading clubs from Hockey Hong Kong...</p>
</div>
`;
modal.show();
// Fetch clubs
fetch('/admin/api/clubs')
.then(response => response.json())
.then(data => {
if (data.success) {
previewClubs = data.clubs;
displayPreviewClubs(data.clubs);
} else {
content.innerHTML = `
<div class="alert alert-warning">
<h6>Unable to fetch clubs</h6>
<p>${data.message}</p>
<p><small>This might be due to website structure changes or network issues.</small></p>
</div>
`;
}
})
.catch(error => {
console.error('Error:', error);
content.innerHTML = `
<div class="alert alert-danger">
<h6>Error loading clubs</h6>
<p>There was an error fetching clubs from the Hockey Hong Kong website.</p>
</div>
`;
});
}
function displayPreviewClubs(clubs) {
const content = document.getElementById('previewContent');
if (clubs.length === 0) {
content.innerHTML = `
<div class="alert alert-info">
<h6>No clubs found</h6>
<p>The website structure may have changed or no clubs are currently listed.</p>
</div>
`;
return;
}
let html = `
<div class="alert alert-info">
<h6>Found ${clubs.length} clubs</h6>
<p>These clubs will be imported with their full names and abbreviations.</p>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Club Name</th>
<th>Abbreviation</th>
<th>Teams</th>
</tr>
</thead>
<tbody>
`;
clubs.forEach(club => {
const teams = club.teams ? club.teams.join(', ') : 'N/A';
html += `
<tr>
<td>${club.name}</td>
<td><span class="badge bg-secondary">${club.abbreviation || 'N/A'}</span></td>
<td>${teams}</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
content.innerHTML = html;
}
function confirmImport() {
const importBtn = document.getElementById('confirmImportBtn');
const spinner = document.getElementById('importSpinner');
// Show loading state
importBtn.disabled = true;
importBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Importing...';
// Import clubs
fetch('/admin/api/import-clubs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showStatus(data.message, 'success');
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('previewModal'));
modal.hide();
// Reload page to show updated clubs
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
showStatus(data.message, 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showStatus('Error importing clubs', 'danger');
})
.finally(() => {
importBtn.disabled = false;
importBtn.innerHTML = 'Import All';
});
}
function importClubs() {
const importBtn = document.getElementById('importClubsBtn');
const spinner = document.getElementById('importSpinner');
// Show loading state
importBtn.disabled = true;
spinner.classList.remove('d-none');
// Import clubs directly
fetch('/admin/api/import-clubs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showStatus(data.message, 'success');
// Reload page to show updated clubs
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
showStatus(data.message, 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showStatus('Error importing clubs', 'danger');
})
.finally(() => {
importBtn.disabled = false;
spinner.classList.add('d-none');
});
}
</script>
</body>
</html>

View File

@ -1,174 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Select Clubs to Import - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-10">
<div class="card">
<div class="card-header">
<h3>Select Clubs to Import</h3>
<p class="mb-0 text-muted">Choose which clubs you want to import from the Hong Kong Hockey Association</p>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="alert alert-info">
<h5>Club Selection</h5>
<p>Select the clubs you want to import. You can choose all clubs or select specific ones based on your needs.</p>
<p><strong>Note:</strong> Only new clubs will be imported. Existing clubs will be skipped to prevent duplicates.</p>
</div>
{% if clubs %}
<form method="POST" id="clubSelectionForm">
{{ form.hidden_tag() }}
<div class="mb-3">
<div class="d-flex gap-2 mb-3">
<button type="submit" name="select_all" class="btn btn-outline-primary btn-sm" formnovalidate>
Select All
</button>
<button type="submit" name="select_none" class="btn btn-outline-secondary btn-sm" formnovalidate>
Select None
</button>
<span class="text-muted align-self-center">
<span id="selectedCount">0</span> of {{ clubs|length }} clubs selected
</span>
</div>
</div>
<div class="row">
{% for club in clubs %}
<div class="col-md-6 col-lg-4 mb-3">
<div class="card h-100">
<div class="card-body">
<div class="form-check">
<input class="form-check-input club-checkbox"
type="checkbox"
name="selected_clubs"
value="{{ club.name }}"
id="club_{{ loop.index }}"
{% if club.name in selected_clubs %}checked{% endif %}>
<label class="form-check-label w-100" for="club_{{ loop.index }}">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">{{ club.name }}</h6>
{% if club.abbreviation %}
<small class="text-muted">{{ club.abbreviation }}</small>
{% endif %}
</div>
<div class="text-end">
{% if club.teams %}
<small class="badge bg-info">{{ club.teams|length }} teams</small>
{% endif %}
</div>
</div>
{% if club.convenor %}
<div class="mt-2">
<small class="text-muted">
<i class="bi bi-person"></i> {{ club.convenor }}
</small>
</div>
{% endif %}
{% if club.email %}
<div>
<small class="text-muted">
<i class="bi bi-envelope"></i> {{ club.email }}
</small>
</div>
{% endif %}
</label>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-4">
<button type="submit" name="cancel" class="btn btn-secondary me-md-2" formnovalidate>
Cancel
</button>
<button type="submit" name="import_selected" class="btn btn-primary" id="importButton" disabled>
Import Selected Clubs
</button>
</div>
</form>
{% else %}
<div class="alert alert-warning">
<h5>No clubs found</h5>
<p>Unable to fetch clubs from the Hong Kong Hockey Association website. This might be due to:</p>
<ul>
<li>Network connectivity issues</li>
<li>Website structure changes</li>
<li>Server maintenance</li>
</ul>
<p>Please try again later or contact the administrator.</p>
</div>
{% endif %}
</div>
</div>
<div class="mt-3">
<a href="/admin/import" class="btn btn-outline-secondary">Back to Data Import</a>
<a href="/admin" class="btn btn-outline-secondary">Back to Admin</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Update selected count and enable/disable import button
function updateSelection() {
const checkboxes = document.querySelectorAll('.club-checkbox');
const selectedCount = document.querySelectorAll('.club-checkbox:checked').length;
const importButton = document.getElementById('importButton');
const countSpan = document.getElementById('selectedCount');
countSpan.textContent = selectedCount;
importButton.disabled = selectedCount === 0;
}
// Add event listeners to checkboxes
document.addEventListener('DOMContentLoaded', function() {
const checkboxes = document.querySelectorAll('.club-checkbox');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', updateSelection);
});
updateSelection(); // Initial update
});
// Handle select all/none buttons
document.getElementById('clubSelectionForm').addEventListener('submit', function(e) {
if (e.submitter.name === 'select_all') {
e.preventDefault();
document.querySelectorAll('.club-checkbox').forEach(checkbox => {
checkbox.checked = true;
});
updateSelection();
} else if (e.submitter.name === 'select_none') {
e.preventDefault();
document.querySelectorAll('.club-checkbox').forEach(checkbox => {
checkbox.checked = false;
});
updateSelection();
}
});
</script>
</body>
</html>

View File

@ -1,128 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Import - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3>Import Data from Hong Kong Hockey Association</h3>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="alert alert-info">
<h5>Data Source</h5>
<p>This import feature populates the database with clubs and teams from the <a href="https://hockey.org.hk/MenStanding.asp" target="_blank">Hong Kong Hockey Association Men's League Standings</a>.</p>
<p><strong>Note:</strong> Only new records will be imported. Existing clubs and teams will be skipped to prevent duplicates.</p>
</div>
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-4">
<h5>Import Options</h5>
<div class="form-check mb-3">
{{ form.import_clubs(class="form-check-input") }}
{{ form.import_clubs.label(class="form-check-label") }}
<small class="form-text text-muted d-block">
Import 30+ hockey clubs including HKFC, KCC, USRC, Valley, SSSC, Dragons, etc.
</small>
<div class="mt-2">
<a href="/admin/import/clubs/select" class="btn btn-outline-primary btn-sm">
Select Specific Clubs
</a>
<small class="text-muted d-block mt-1">
Choose which clubs to import instead of importing all
</small>
</div>
</div>
<div class="form-check mb-3">
{{ form.import_teams(class="form-check-input") }}
{{ form.import_teams.label(class="form-check-label") }}
<small class="form-text text-muted d-block">
Import teams from all 6 divisions (Premier, 1st, 2nd, 3rd, 4th, 5th, 6th Division)
</small>
</div>
<div class="form-check mb-3">
{{ form.import_players(class="form-check-input") }}
{{ form.import_players.label(class="form-check-label") }}
<small class="form-text text-muted d-block">
Import 5 sample players for HKFC C team (optional)
</small>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
{{ form.cancel(class="btn btn-secondary me-md-2") }}
{{ form.import_data(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
<div class="mt-3">
<a href="/admin" class="btn btn-outline-secondary">Back to Admin</a>
</div>
<div class="mt-4">
<div class="card">
<div class="card-header">
<h5>What Gets Imported</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Clubs (30+ clubs)</h6>
<ul class="list-unstyled">
<li>• HKFC, KCC, USRC, Valley</li>
<li>• SSSC, Dragons, Kai Tak, RHOBA</li>
<li>• Elite, Aquila, HKJ, Sirius</li>
<li>• Shaheen, Diocesan, Rhino, Khalsa</li>
<li>• HKCC, Police, Recreio, CSD</li>
<li>• Dutch, HKUHC, Kaitiaki, Antlers</li>
<li>• Marcellin, Skyers, JR, IUHK</li>
<li>• 144U, HKU, and more...</li>
</ul>
</div>
<div class="col-md-6">
<h6>Teams (60+ teams across 6 divisions)</h6>
<ul class="list-unstyled">
<li><strong>Premier Division:</strong> HKFC A, KCC A, USRC A, Valley A</li>
<li><strong>1st Division:</strong> HKFC B, KCC B, USRC B, Valley B</li>
<li><strong>2nd Division:</strong> HKFC C, KCC C, USRC C, Valley C</li>
<li><strong>3rd Division:</strong> SSSC C, Dragons A, Kai Tak B, etc.</li>
<li><strong>4th Division:</strong> Khalsa C, HKCC C, Valley D, etc.</li>
<li><strong>5th Division:</strong> KCC D, Kai Tak C, Dragons B, etc.</li>
<li><strong>6th Division:</strong> Rhino B, Skyers A, JR, etc.</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,260 +0,0 @@
{% extends "bootstrap/base.html" %}
{% block title %}Database Setup - MOTM Admin{% endblock %}
{% block head %}
{{ super() }}
<style>
.database-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
}
.database-section h3 {
margin-top: 0;
color: #333;
}
.form-group {
margin-bottom: 15px;
}
.btn-group {
margin-top: 20px;
}
.btn {
margin-right: 10px;
}
.alert {
margin-top: 20px;
}
.hidden {
display: none;
}
.current-config {
background-color: #e8f4f8;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.current-config h4 {
margin-top: 0;
color: #2c3e50;
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Database Setup & Configuration</h1>
<p class="lead">Configure and initialize the database for the MOTM application.</p>
<!-- Current Configuration Display -->
<div class="current-config">
<h4>Current Configuration</h4>
<p><strong>Database Type:</strong> {{ current_config.database_type|title }}</p>
{% if current_config.database_type == 'sqlite' %}
<p><strong>Database File:</strong> {{ current_config.sqlite_database_path }}</p>
{% elif current_config.database_type == 'mysql' %}
<p><strong>Host:</strong> {{ current_config.mysql_host }}:{{ current_config.mysql_port }}</p>
<p><strong>Database:</strong> {{ current_config.mysql_database }}</p>
<p><strong>Username:</strong> {{ current_config.mysql_username }}</p>
{% elif current_config.database_type == 'postgresql' %}
<p><strong>Host:</strong> {{ current_config.postgres_host }}:{{ current_config.postgres_port }}</p>
<p><strong>Database:</strong> {{ current_config.postgres_database }}</p>
<p><strong>Username:</strong> {{ current_config.postgres_username }}</p>
{% endif %}
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Database Setup Form -->
<form method="POST" action="{{ url_for('database_setup') }}">
{{ form.hidden_tag() }}
<!-- Database Type Selection -->
<div class="database-section">
<h3>Database Type</h3>
<div class="form-group">
{{ form.database_type.label(class="control-label") }}
{{ form.database_type(class="form-control") }}
</div>
</div>
<!-- SQLite Configuration -->
<div class="database-section" id="sqlite-config">
<h3>SQLite Configuration</h3>
<div class="form-group">
{{ form.sqlite_database_path.label(class="control-label") }}
{{ form.sqlite_database_path(class="form-control") }}
<small class="form-text text-muted">Path to the SQLite database file</small>
</div>
</div>
<!-- MySQL Configuration -->
<div class="database-section" id="mysql-config">
<h3>MySQL/MariaDB Configuration</h3>
<div class="row">
<div class="col-md-6">
<div class="form-group">
{{ form.mysql_host.label(class="control-label") }}
{{ form.mysql_host(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.mysql_port.label(class="control-label") }}
{{ form.mysql_port(class="form-control") }}
</div>
</div>
</div>
<div class="form-group">
{{ form.mysql_database.label(class="control-label") }}
{{ form.mysql_database(class="form-control") }}
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
{{ form.mysql_username.label(class="control-label") }}
{{ form.mysql_username(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.mysql_password.label(class="control-label") }}
{{ form.mysql_password(class="form-control") }}
</div>
</div>
</div>
<div class="form-group">
{{ form.mysql_charset.label(class="control-label") }}
{{ form.mysql_charset(class="form-control") }}
</div>
</div>
<!-- PostgreSQL Configuration -->
<div class="database-section" id="postgresql-config">
<h3>PostgreSQL Configuration</h3>
<div class="row">
<div class="col-md-6">
<div class="form-group">
{{ form.postgres_host.label(class="control-label") }}
{{ form.postgres_host(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.postgres_port.label(class="control-label") }}
{{ form.postgres_port(class="form-control") }}
</div>
</div>
</div>
<div class="form-group">
{{ form.postgres_database.label(class="control-label") }}
{{ form.postgres_database(class="form-control") }}
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
{{ form.postgres_username.label(class="control-label") }}
{{ form.postgres_username(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.postgres_password.label(class="control-label") }}
{{ form.postgres_password(class="form-control") }}
</div>
</div>
</div>
</div>
<!-- Setup Options -->
<div class="database-section">
<h3>Setup Options</h3>
<div class="form-group">
<div class="checkbox">
<label>
{{ form.initialize_tables() }} {{ form.initialize_tables.label.text }}
</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
{{ form.create_sample_data() }} {{ form.create_sample_data.label.text }}
</label>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="btn-group">
{{ form.test_connection(class="btn btn-info") }}
{{ form.save_config(class="btn btn-primary") }}
{{ form.initialize_database(class="btn btn-success") }}
</div>
</form>
<!-- Navigation -->
<div class="row" style="margin-top: 30px;">
<div class="col-md-12">
<a href="{{ url_for('database_status') }}" class="btn btn-default">View Database Status</a>
<a href="{{ url_for('motm_admin') }}" class="btn btn-default">Back to MOTM Admin</a>
</div>
</div>
</div>
</div>
</div>
<script>
// Show/hide database configuration sections based on selected type
function toggleDatabaseConfig() {
var dbType = document.getElementById('database_type').value;
// Hide all config sections
document.getElementById('sqlite-config').style.display = 'none';
document.getElementById('mysql-config').style.display = 'none';
document.getElementById('postgresql-config').style.display = 'none';
// Show relevant config section
if (dbType === 'sqlite') {
document.getElementById('sqlite-config').style.display = 'block';
} else if (dbType === 'mysql') {
document.getElementById('mysql-config').style.display = 'block';
} else if (dbType === 'postgresql') {
document.getElementById('postgresql-config').style.display = 'block';
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
toggleDatabaseConfig();
// Add event listener for database type changes
document.getElementById('database_type').addEventListener('change', toggleDatabaseConfig);
});
</script>
{% endblock %}

View File

@ -1,162 +0,0 @@
{% extends "bootstrap/base.html" %}
{% block title %}Database Status - MOTM Admin{% endblock %}
{% block head %}
{{ super() }}
<style>
.status-card {
margin-bottom: 20px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
}
.status-card h3 {
margin-top: 0;
color: #333;
}
.status-item {
margin-bottom: 10px;
padding: 10px;
background-color: white;
border-radius: 3px;
border-left: 4px solid #3498db;
}
.status-item.success {
border-left-color: #27ae60;
}
.status-item.error {
border-left-color: #e74c3c;
}
.status-item.warning {
border-left-color: #f39c12;
}
.status-label {
font-weight: bold;
color: #2c3e50;
}
.status-value {
color: #7f8c8d;
word-break: break-all;
}
.action-buttons {
margin-top: 30px;
text-align: center;
}
.btn {
margin: 0 10px;
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Database Status</h1>
<p class="lead">Current database configuration and connection status.</p>
<!-- Database Status Card -->
<div class="status-card">
<h3>Database Information</h3>
<div class="status-item {{ 'success' if 'Connected' in db_info.connection_status else 'error' }}">
<div class="status-label">Connection Status:</div>
<div class="status-value">{{ db_info.connection_status }}</div>
</div>
<div class="status-item">
<div class="status-label">Database Type:</div>
<div class="status-value">{{ db_info.database_type|title }}</div>
</div>
<div class="status-item">
<div class="status-label">Database URL:</div>
<div class="status-value">{{ db_info.database_url }}</div>
</div>
<div class="status-item">
<div class="status-label">Tables Count:</div>
<div class="status-value">{{ db_info.table_count }}</div>
</div>
</div>
<!-- Quick Actions -->
<div class="status-card">
<h3>Quick Actions</h3>
<div class="action-buttons">
<a href="{{ url_for('database_setup') }}" class="btn btn-primary">
<i class="glyphicon glyphicon-cog"></i> Database Setup
</a>
<a href="{{ url_for('motm_admin') }}" class="btn btn-default">
<i class="glyphicon glyphicon-arrow-left"></i> Back to MOTM Admin
</a>
</div>
</div>
<!-- Database Tables Information -->
{% if db_info.table_count != 'Unknown' and db_info.table_count > 0 %}
<div class="status-card">
<h3>Database Tables</h3>
<p>The database contains {{ db_info.table_count }} tables. The following tables are available:</p>
<ul>
<li><strong>players</strong> - Player information and details</li>
<li><strong>clubs</strong> - Hockey club information</li>
<li><strong>teams</strong> - Team information and league details</li>
<li><strong>match_squad</strong> - Match squad selections</li>
<li><strong>hockey_fixtures</strong> - Match fixtures and results</li>
<li><strong>admin_settings</strong> - Application configuration</li>
<li><strong>motm_votes</strong> - Man of the Match voting data</li>
<li><strong>match_comments</strong> - Match comments and feedback</li>
<li><strong>hockey_users</strong> - User authentication data</li>
</ul>
</div>
{% endif %}
<!-- Configuration Help -->
<div class="status-card">
<h3>Configuration Help</h3>
<div class="row">
<div class="col-md-4">
<h4>SQLite</h4>
<p>Best for development and small deployments. No server required.</p>
<ul>
<li>File-based database</li>
<li>No installation required</li>
<li>Good for testing</li>
</ul>
</div>
<div class="col-md-4">
<h4>MySQL/MariaDB</h4>
<p>Popular choice for web applications. Good performance and reliability.</p>
<ul>
<li>Server-based database</li>
<li>Good for production</li>
<li>Wide hosting support</li>
</ul>
</div>
<div class="col-md-4">
<h4>PostgreSQL</h4>
<p>Advanced features and excellent performance. Great for complex applications.</p>
<ul>
<li>Advanced SQL features</li>
<li>Excellent performance</li>
<li>Strong data integrity</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,230 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Device Tracking - HKFC Men's C Team MOTM System</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.tracking-section {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 2rem;
}
.pattern-warning {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1rem;
}
.device-id {
font-family: monospace;
font-size: 0.9em;
background-color: #e9ecef;
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
}
</style>
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<h1>Device Tracking Analysis</h1>
<p class="lead">Monitor voting patterns and detect potential duplicate voting</p>
<div class="mb-3">
<a href="/admin" class="btn btn-outline-primary">Back to Admin Dashboard</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Analysis Controls -->
<div class="tracking-section">
<h3>Analysis Controls</h3>
<div class="row">
<div class="col-md-4">
<form method="POST">
<input type="hidden" name="action" value="analyze_patterns">
<button type="submit" class="btn btn-primary">Analyze Voting Patterns</button>
</form>
<small class="text-muted">Find devices with multiple votes</small>
</div>
<div class="col-md-4">
<a href="/admin/device-tracking" class="btn btn-outline-secondary">View Recent Votes</a>
<small class="text-muted">Show last 50 votes</small>
</div>
</div>
</div>
<!-- Pattern Analysis Results -->
{% if analysis_mode and patterns %}
<div class="tracking-section">
<h3>Voting Pattern Analysis</h3>
<p class="text-muted">Devices that have voted multiple times:</p>
{% if patterns %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Device ID</th>
<th>Vote Count</th>
<th>Fixtures</th>
<th>MOTM Players</th>
<th>DotD Players</th>
<th>First Vote</th>
<th>Last Vote</th>
<th>IP Addresses</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for pattern in patterns %}
<tr>
<td><span class="device-id">{{ pattern.device_id }}</span></td>
<td>
<span class="badge bg-{{ 'danger' if pattern.vote_count > 3 else 'warning' }}">
{{ pattern.vote_count }}
</span>
</td>
<td>{{ pattern.fixtures_voted }}</td>
<td>{{ pattern.motm_players }}</td>
<td>{{ pattern.dotd_players }}</td>
<td>{{ pattern.first_vote.strftime('%Y-%m-%d %H:%M') if pattern.first_vote else 'N/A' }}</td>
<td>{{ pattern.last_vote.strftime('%Y-%m-%d %H:%M') if pattern.last_vote else 'N/A' }}</td>
<td>{{ pattern.ip_addresses }}</td>
<td>
<form method="POST" style="display: inline;">
<input type="hidden" name="action" value="view_device_details">
<input type="hidden" name="device_id" value="{{ pattern.device_id }}">
<button type="submit" class="btn btn-sm btn-outline-info">Details</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if patterns|length > 0 %}
<div class="pattern-warning">
<h5><i class="bi bi-exclamation-triangle"></i> Pattern Analysis</h5>
<p class="mb-0">
<strong>Warning:</strong> {{ patterns|length }} device(s) have voted multiple times.
This could indicate duplicate voting or shared devices.
</p>
</div>
{% endif %}
{% else %}
<div class="alert alert-success">
<h5>No Suspicious Patterns Found</h5>
<p>All devices have voted only once per fixture.</p>
</div>
{% endif %}
</div>
{% endif %}
<!-- Device Details -->
{% if details_mode and device_details %}
<div class="tracking-section">
<h3>Device Details: <span class="device-id">{{ selected_device }}</span></h3>
<p class="text-muted">Complete voting history for this device:</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Fixture Date</th>
<th>MOTM Vote</th>
<th>DotD Vote</th>
<th>IP Address</th>
<th>Vote Time</th>
</tr>
</thead>
<tbody>
{% for vote in device_details %}
<tr>
<td>{{ vote.fixture_date }}</td>
<td>{{ vote.motm_player_name }}</td>
<td>{{ vote.dotd_player_name }}</td>
<td>{{ vote.ip_address }}</td>
<td>{{ vote.vote_timestamp.strftime('%Y-%m-%d %H:%M:%S') if vote.vote_timestamp else 'N/A' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="mt-3">
<h5>Device Information</h5>
{% if device_details %}
<p><strong>User Agent:</strong> {{ device_details[0].user_agent[:100] }}{% if device_details[0].user_agent|length > 100 %}...{% endif %}</p>
<p><strong>Total Votes:</strong> {{ device_details|length }}</p>
<p><strong>Unique Fixtures:</strong> {{ device_details|map(attribute='fixture_date')|unique|list|length }}</p>
{% endif %}
</div>
</div>
{% endif %}
<!-- Recent Votes -->
{% if recent_votes and not analysis_mode and not details_mode %}
<div class="tracking-section">
<h3>Recent Votes</h3>
<p class="text-muted">Last 50 votes cast:</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Device ID</th>
<th>Fixture Date</th>
<th>MOTM Vote</th>
<th>DotD Vote</th>
<th>IP Address</th>
<th>Vote Time</th>
</tr>
</thead>
<tbody>
{% for vote in recent_votes %}
<tr>
<td><span class="device-id">{{ vote.device_id }}</span></td>
<td>{{ vote.fixture_date }}</td>
<td>{{ vote.motm_player_name }}</td>
<td>{{ vote.dotd_player_name }}</td>
<td>{{ vote.ip_address }}</td>
<td>{{ vote.vote_timestamp.strftime('%Y-%m-%d %H:%M') if vote.vote_timestamp else 'N/A' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- No Data Message -->
{% if not recent_votes and not patterns and not device_details %}
<div class="alert alert-info">
<h5>No Vote Data Available</h5>
<p>No votes have been cast yet, or the device tracking table is empty.</p>
</div>
{% endif %}
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,334 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Club - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3>Edit Club #{{ club_id }}</h3>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.hockey_club.label(class="form-label") }}
{{ form.hockey_club(class="form-control") }}
{% if form.hockey_club.errors %}
<div class="text-danger">
{% for error in form.hockey_club.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.logo_url.label(class="form-label") }}
<div class="input-group">
{{ form.logo_url(class="form-control", id="logoUrl", onchange="previewLogo()") }}
<button type="button" class="btn btn-outline-secondary" onclick="browseS3()" title="Browse S3 Storage">
<i class="fas fa-cloud"></i> Browse S3
</button>
</div>
{% if form.logo_url.errors %}
<div class="text-danger">
{% for error in form.logo_url.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">Enter the full URL to the club's logo image or use the S3 browser to select from your configured storage.</small>
<!-- Logo Preview -->
<div id="logoPreview" class="mt-2" style="display: none;">
<label class="form-label small">Preview:</label>
<div class="border rounded p-2 bg-light">
<img id="previewImage" src="" alt="Logo preview" style="max-height: 80px; max-width: 120px;" onerror="this.style.display='none'; document.getElementById('previewError').style.display='block';" onload="document.getElementById('previewError').style.display='none';">
<div id="previewError" class="text-muted small" style="display: none;">Unable to load image</div>
</div>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
{{ form.cancel(class="btn btn-secondary me-md-2") }}
{{ form.save_club(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
<!-- S3 Browser Modal -->
<div class="modal fade" id="s3BrowserModal" tabindex="-1" aria-labelledby="s3BrowserModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="s3BrowserModalLabel">Browse S3 Storage</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="s3BrowserContent">
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p>Loading S3 contents...</p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="selectS3FileBtn" onclick="selectS3File()" disabled>Select File</button>
</div>
</div>
</div>
</div>
<div class="mt-3">
<a href="/admin/clubs" class="btn btn-outline-secondary">Back to Club Management</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
function previewLogo() {
const urlInput = document.getElementById('logoUrl');
const previewDiv = document.getElementById('logoPreview');
const previewImg = document.getElementById('previewImage');
if (urlInput.value.trim()) {
previewImg.src = urlInput.value;
previewDiv.style.display = 'block';
} else {
previewDiv.style.display = 'none';
}
}
// S3 Browser functionality
let selectedS3File = null;
let currentS3Path = '';
function browseS3() {
// Reset state
selectedS3File = null;
currentS3Path = '';
// Show loading state
document.getElementById('s3BrowserContent').innerHTML = `
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p>Loading S3 contents...</p>
</div>
`;
// Show modal
const s3BrowserModal = new bootstrap.Modal(document.getElementById('s3BrowserModal'));
s3BrowserModal.show();
// Load S3 contents from root of assets folder
loadS3Contents('');
}
function loadS3Contents(path) {
fetch(`/admin/api/s3-browser?path=${encodeURIComponent(path)}`)
.then(response => response.json())
.then(data => {
if (data.success) {
displayS3Contents(data);
} else {
document.getElementById('s3BrowserContent').innerHTML = `
<div class="alert alert-danger" role="alert">
<strong>Error:</strong> ${data.message}
</div>
`;
}
})
.catch(error => {
console.error('Error:', error);
document.getElementById('s3BrowserContent').innerHTML = `
<div class="alert alert-danger" role="alert">
<strong>Error:</strong> Failed to load S3 contents. Please try again.
</div>
`;
});
}
function displayS3Contents(data) {
currentS3Path = data.path;
let html = `
<div class="row mb-3">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="#" onclick="loadS3Contents('')">assets</a>
</li>
`;
// Build breadcrumb
if (data.path !== '') {
const pathParts = data.path.split('/').filter(p => p);
let currentPath = '';
pathParts.forEach((part, index) => {
currentPath += part + '/';
const isLast = index === pathParts.length - 1;
html += `<li class="breadcrumb-item ${isLast ? 'active' : ''}">`;
if (!isLast) {
html += `<a href="#" onclick="loadS3Contents('${currentPath}')">${part}</a>`;
} else {
html += part;
}
html += '</li>';
});
}
html += `
</ol>
</nav>
</div>
</div>
<div class="row">
`;
// Display folders
if (data.folders.length > 0) {
data.folders.forEach(folder => {
html += `
<div class="col-md-3 mb-3">
<div class="card h-100 folder-card" onclick="loadS3Contents('${folder.path}')" style="cursor: pointer;">
<div class="card-body text-center">
<i class="fas fa-folder fa-3x text-warning mb-2"></i>
<h6 class="card-title">${folder.name}</h6>
</div>
</div>
</div>
`;
});
}
// Display files
if (data.files.length > 0) {
data.files.forEach(file => {
const isImage = /\.(jpg|jpeg|png|gif|svg|webp)$/i.test(file.name);
const fileSize = formatFileSize(file.size);
html += `
<div class="col-md-3 mb-3">
<div class="card h-100 file-card" onclick="selectS3FileItem('${file.path}', '${file.url}')" style="cursor: pointer;">
<div class="card-body text-center">
`;
if (isImage) {
html += `
<img src="${file.url}" alt="${file.name}" class="img-fluid mb-2" style="max-height: 100px; max-width: 100%; object-fit: contain;">
`;
} else {
html += `
<i class="fas fa-file fa-3x text-primary mb-2"></i>
`;
}
html += `
<h6 class="card-title">${file.name}</h6>
<small class="text-muted">${fileSize}</small>
</div>
</div>
</div>
`;
});
}
if (data.folders.length === 0 && data.files.length === 0) {
html += `
<div class="col-12">
<div class="alert alert-info text-center">
<i class="fas fa-info-circle"></i> No files or folders found in this directory.
</div>
</div>
`;
}
html += `
</div>
`;
document.getElementById('s3BrowserContent').innerHTML = html;
}
function selectS3FileItem(filePath, fileUrl) {
// Remove previous selection
document.querySelectorAll('.file-card').forEach(card => {
card.classList.remove('border-primary');
});
// Add selection to clicked card
event.currentTarget.classList.add('border-primary');
// Store selected file
selectedS3File = {
path: filePath,
url: fileUrl
};
// Enable select button
document.getElementById('selectS3FileBtn').disabled = false;
}
function selectS3File() {
if (selectedS3File) {
// Update the logo URL field
const logoUrlField = document.getElementById('logoUrl');
if (logoUrlField) {
logoUrlField.value = selectedS3File.path;
// Trigger preview update
previewLogo();
}
// Close modal
const s3BrowserModal = bootstrap.Modal.getInstance(document.getElementById('s3BrowserModal'));
s3BrowserModal.hide();
}
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Preview logo on page load if URL is already filled
document.addEventListener('DOMContentLoaded', function() {
previewLogo();
});
</script>
</body>
</html>

View File

@ -1,103 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Player - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3>Edit Player #{{ player_number }}</h3>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.player_number.label(class="form-label") }}
{{ form.player_number(class="form-control", readonly=true) }}
<small class="form-text text-muted">Player number cannot be changed</small>
</div>
<div class="mb-3">
{{ form.player_forenames.label(class="form-label") }}
{{ form.player_forenames(class="form-control") }}
{% if form.player_forenames.errors %}
<div class="text-danger">
{% for error in form.player_forenames.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.player_surname.label(class="form-label") }}
{{ form.player_surname(class="form-control") }}
{% if form.player_surname.errors %}
<div class="text-danger">
{% for error in form.player_surname.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.player_nickname.label(class="form-label") }}
{{ form.player_nickname(class="form-control") }}
{% if form.player_nickname.errors %}
<div class="text-danger">
{% for error in form.player_nickname.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.player_team.label(class="form-label") }}
{{ form.player_team(class="form-select") }}
{% if form.player_team.errors %}
<div class="text-danger">
{% for error in form.player_team.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
{{ form.cancel(class="btn btn-secondary me-md-2") }}
{{ form.save_player(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
<div class="mt-3">
<a href="/admin/players" class="btn btn-outline-secondary">Back to Player Management</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,101 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Team - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3>Edit Team #{{ team_id }}</h3>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.club.label(class="form-label") }}
{{ form.club(class="form-select") }}
{% if form.club.errors %}
<div class="text-danger">
{% for error in form.club.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">Select the club from the list of available clubs</small>
</div>
<div class="mb-3">
{{ form.team.label(class="form-label") }}
{{ form.team(class="form-control") }}
{% if form.team.errors %}
<div class="text-danger">
{% for error in form.team.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">Enter the team identifier (e.g., A, B, C, 1st, 2nd)</small>
</div>
<div class="mb-3">
{{ form.display_name.label(class="form-label") }}
{{ form.display_name(class="form-control") }}
{% if form.display_name.errors %}
<div class="text-danger">
{% for error in form.display_name.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">Enter the full display name (e.g., HKFC A, KCC 1st Team)</small>
</div>
<div class="mb-3">
{{ form.league.label(class="form-label") }}
{{ form.league(class="form-select") }}
{% if form.league.errors %}
<div class="text-danger">
{% for error in form.league.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">Select the league/division from the list</small>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
{{ form.cancel(class="btn btn-secondary me-md-2") }}
{{ form.save_team(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
<div class="mt-3">
<a href="/admin/teams" class="btn btn-outline-secondary">Back to Team Management</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,19 +0,0 @@
<html>
<head>
<title>Error - Invalid URL</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Error</h1>
<p>{{ message or "Invalid voting URL. Please check the link and try again." }}</p>
<a class="btn btn-primary" href="/" role="button">Home</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,69 +0,0 @@
<html>
<head>
<title>Goals and Assists Admin</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<body>
<h3>Goals and Assists Administration</h3>
<form method="post" action="/admin/stats/submit">
{{ form.csrf_token }}
<div class="row">
<div class="col-sm-6">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Match:</span>
{{ form.match(class_="form-control") }}
</div>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<thead>
<tr>
<th>Player Name</th>
<th>Goals</th>
<th>Assists</th>
</tr>
</thead>
<tbody>
{% for player in data %}
<tr>
<td>
<input type="hidden" name="playerName" value="{{ player.playerNickname }}">
<input type="hidden" name="playerNumber" value="{{ player.playerNumber }}">
{{ player.playerNickname }}
</td>
<td>
<select name="goals" class="form-control">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</td>
<td>
<select name="assists" class="form-control">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<button type="submit" class="btn btn-success">Submit</button>
<a class="btn btn-danger" href="/" role="button">Cancel</a>
</form>
</body>
</html>

View File

@ -1,19 +0,0 @@
<html>
<head>
<title>Goals and Assists Submitted</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h3>Goals and Assists have been recorded</h3>
<a class="btn btn-primary" href="/admin/stats" role="button">Add More Stats</a>
<a class="btn btn-danger" href="/" role="button">Home</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,115 +0,0 @@
<html>
<head>
<title>HKFC Men's C Team - MOTM System</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>HKFC Men's C Team - Man of the Match System</h1>
<div class="jumbotron">
<h2>Welcome to the MOTM Voting System</h2>
<p>This system allows players to vote for Man of the Match and Dick of the Day, while providing admin tools for managing matches and squads.</p>
</div>
<div class="row">
<div class="col-md-6">
<h3>Player Section</h3>
<div class="list-group">
<a href="/motm/comments" class="list-group-item">
<h4 class="list-group-item-heading">Match Comments</h4>
<p class="list-group-item-text">View comments from recent matches</p>
</a>
</div>
</div>
{% if is_admin %}
<div class="col-md-6">
<h3>Admin Section</h3>
<div class="list-group">
<a href="/admin" class="list-group-item">
<h4 class="list-group-item-heading">Admin Dashboard</h4>
<p class="list-group-item-text">Access all administrative functions</p>
</a>
<a href="/admin/players" class="list-group-item">
<h4 class="list-group-item-heading">Player Management</h4>
<p class="list-group-item-text">Add, edit, and manage players in the database</p>
</a>
<a href="/admin/clubs" class="list-group-item">
<h4 class="list-group-item-heading">Club Management</h4>
<p class="list-group-item-text">Add, edit, and manage hockey clubs</p>
</a>
<a href="/admin/teams" class="list-group-item">
<h4 class="list-group-item-heading">Team Management</h4>
<p class="list-group-item-text">Add, edit, and manage hockey teams</p>
</a>
<a href="/admin/import" class="list-group-item">
<h4 class="list-group-item-heading">Data Import</h4>
<p class="list-group-item-text">Import clubs and teams from Hong Kong Hockey Association</p>
</a>
<a href="/admin/squad" class="list-group-item">
<h4 class="list-group-item-heading">Match Squad Selection</h4>
<p class="list-group-item-text">Select players for the match squad</p>
</a>
<a href="/admin/squad/list" class="list-group-item">
<h4 class="list-group-item-heading">View Current Squad</h4>
<p class="list-group-item-text">View current match squad</p>
</a>
<a href="/admin/squad/reset" class="list-group-item">
<h4 class="list-group-item-heading">Reset Squad</h4>
<p class="list-group-item-text">Reset squad for new match</p>
</a>
<a href="/admin/motm" class="list-group-item">
<h4 class="list-group-item-heading">MOTM Admin</h4>
<p class="list-group-item-text">Manage match settings and activate voting</p>
</a>
<a href="/admin/stats" class="list-group-item">
<h4 class="list-group-item-heading">Goals & Assists</h4>
<p class="list-group-item-text">Record goals and assists statistics</p>
</a>
<a href="/admin/voting" class="list-group-item">
<h4 class="list-group-item-heading">Voting Results</h4>
<p class="list-group-item-text">View current match voting results</p>
</a>
<a href="/admin/poty" class="list-group-item">
<h4 class="list-group-item-heading">Player of the Year</h4>
<p class="list-group-item-text">View season totals and Player of the Year standings</p>
</a>
<a href="/admin/database-setup" class="list-group-item">
<h4 class="list-group-item-heading">Database Setup</h4>
<p class="list-group-item-text">Configure and initialize the database</p>
</a>
<a href="/admin/database-status" class="list-group-item">
<h4 class="list-group-item-heading">Database Status</h4>
<p class="list-group-item-text">View current database configuration and status</p>
</a>
<a href="/admin/s3-config" class="list-group-item">
<h4 class="list-group-item-heading">S3 Configuration</h4>
<p class="list-group-item-text">Configure AWS S3 storage for logos and assets</p>
</a>
<a href="/admin/s3-status" class="list-group-item">
<h4 class="list-group-item-heading">S3 Status</h4>
<p class="list-group-item-text">View current S3 configuration and connection status</p>
</a>
</div>
</div>
{% else %}
<div class="col-md-6">
<h3>Admin Access</h3>
<div class="alert alert-info">
<h4 class="alert-heading">Authentication Required</h4>
<p>Admin functions require authentication. Please contact the system administrator for access.</p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,31 +0,0 @@
<html>
<head>
<title>HKFC Men's C Team - Match Comments</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<body>
<h3>Match Comments</h3>
<div class="row">
<div class="col-sm-6">
<img src="{{ hkfcLogo }}" height="100"></img>
<img src="{{ oppoLogo }}" height="100"></img>
</div>
</div>
<div class="row">
<div class="col-sm-8">
{% for comment in comments %}
<div class="panel panel-default">
<div class="panel-body">
{{ comment.comment }}
</div>
</div>
{% endfor %}
</div>
</div>
<a class="btn btn-primary" href="/" role="button">Home</a>
</body>
</html>

View File

@ -1,73 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Match Squad Selection - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<h1>Match Squad Selection</h1>
<p class="lead">Select players for the match squad from the available players</p>
<div class="mb-3">
<a href="/admin/players" class="btn btn-outline-primary">Manage Players</a>
<a href="/admin" class="btn btn-secondary">Back to Admin</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% if players %}
<form action="/admin/squad/submit" method="post">
<div class="card">
<div class="card-header">
<h5>Available Players</h5>
<small class="text-muted">Select players to add to the match squad</small>
</div>
<div class="card-body">
<div class="row">
{% for player in players %}
<div class="col-md-4 mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="playerNumber" value="{{ player.playernumber }}" id="player{{ player.playernumber }}">
<label class="form-check-label" for="player{{ player.playernumber }}">
<strong>#{{ player.playernumber }}</strong> {{ player.playerforenames }} {{ player.playersurname }}
<br>
<small class="text-muted">{{ player.playernickname }} - {{ player.playerteam }}</small>
</label>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">Add Selected Players to Squad</button>
<a href="/admin/squad/list" class="btn btn-outline-secondary">View Current Squad</a>
</div>
</div>
</form>
{% else %}
<div class="alert alert-warning">
<h5>No players available</h5>
<p>There are no players in the database. <a href="/admin/players/add">Add some players</a> before selecting a squad.</p>
</div>
{% endif %}
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,19 +0,0 @@
<html>
<head>
<title>Squad Reset</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h3>Match squad has been reset for the next match</h3>
<a class="btn btn-primary" href="/admin/squad" role="button">Add Players to New Squad</a>
<a class="btn btn-danger" href="/" role="button">Home</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,40 +0,0 @@
<html>
<head>
<title>HKFC Men's C Team - Match Squad Selected</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h3>Match Squad</h3>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'info' }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="mb-3">
<a class="btn btn-primary" href="/admin/squad" role="button">Add More Players</a>
<a class="btn btn-info" href="/admin/squad/list" role="button">View Squad List</a>
<a class="btn btn-warning" href="/admin/squad/reset" role="button">Reset Squad</a>
<a class="btn btn-secondary" href="/admin" role="button">Back to Admin</a>
<a class="btn btn-danger" href="/" role="button">Cancel</a>
</div>
{{ table }}
</div>
</div>
</div>
</body>
</html>

View File

@ -1,176 +0,0 @@
<html>
<head>
<title>HKFC Men's C Team - MotM and DotD vote admin</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<h2>HKFC Men's C Team MotM and DotD online vote admin page</h2>
<div style="margin-bottom: 15px;">
<a href="/admin" class="btn btn-default btn-sm">
<span class="glyphicon glyphicon-arrow-left"></span> Back to Admin Dashboard
</a>
</div>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<body onload="myFunction()">
<dl>
<p>
{{ form.csrf_token }}
<b>HKFC C Next Opponent:</b>
<br/>
<div class="row">
<div class="col-xs-12">
<form class="col-sm-6" method="post" action="/admin/motm">
<!-- Load Next Fixture Button -->
<div class="row">
<div class="col-sm-12">
<div class="alert alert-info" style="margin-bottom: 15px;">
<button type="button" class="btn btn-info btn-sm" id="loadFixtureBtn" onclick="loadNextFixture()">
<span class="glyphicon glyphicon-download-alt"></span> Load Next HKFC C Fixture
</button>
<a href="https://hockey.org.hk/MenFixture.asp" target="_blank" class="btn btn-default btn-sm" style="margin-left: 5px;">
<span class="glyphicon glyphicon-new-window"></span> View HK Hockey Fixtures
</a>
<span id="fixtureStatus" style="margin-left: 10px;"></span>
<div id="fixtureInfo" style="margin-top: 10px; display: none;"></div>
</div>
</div>
</div>
<div class = "row">
<div class = "col-sm-6">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Date:</span>
{{ form.nextMatchDate(class_="form-control", id="nextMatchDate") }}
</div>
</div>
</div>
</br>
<div class = "row">
<div class = "col-sm-9">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Opposition</span>
{{ form.nextOppoTeam(class_="form-control", id="nextOppoTeam") }}
</div>
</div>
</div>
<div class = "row">
<div class = "col-sm-6">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Current Man of the Match:</span>
{{ form.currMotM(class_="form-control") }}
</div>
</div>
<div class = "col-sm-6">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Current Dick of the Day:</span>
{{ form.currDotD(class_="form-control") }}
</div>
</div>
</div>
{% if not form.currMotM.choices or form.currMotM.choices|length == 0 %}
<div class="row">
<div class="col-sm-12">
<div class="alert alert-warning" style="margin-top: 10px;">
<small><strong>Note:</strong> No players available for previous MOTM/DotD. This is normal if you haven't set up a match squad yet. You can still save the match details.</small>
</div>
</div>
</div>
{% endif %}
<p>
{{ form.saveButton(class_="btn btn-success") }}
{{ form.activateButton(class_="btn btn-primary") }}
<a class="btn btn-danger" href="/" role="button">Cancel</a>
</p>
</form>
<div class="col-sm-4">
<img src="{{ nextOppoLogo }}" height="90" id="nextOppoLogo"/>
</div>
</div>
</div>
</p>
</dl>
<script>
function loadNextFixture() {
// Show loading status
var statusElement = document.getElementById('fixtureStatus');
var infoElement = document.getElementById('fixtureInfo');
var loadBtn = document.getElementById('loadFixtureBtn');
statusElement.innerHTML = '<span class="text-info">Loading...</span>';
loadBtn.disabled = true;
// Fetch the next fixture from the API
fetch('/admin/api/next-fixture')
.then(response => response.json())
.then(data => {
if (data.success) {
// Update the form fields
document.getElementById('nextMatchDate').value = data.date;
document.getElementById('nextOppoTeam').value = data.opponent;
// Show fixture information
let clubInfo = '';
if (data.opponent_club_info) {
const club = data.opponent_club_info;
const confidence = club.match_result ? club.match_result.confidence : 'unknown';
const matchType = club.match_result ? club.match_result.match_type : 'unknown';
clubInfo = '<br><small class="text-muted">';
clubInfo += 'Club: ' + club.club_name;
if (club.logo_url) {
clubInfo += ' | <a href="' + club.logo_url + '" target="_blank">Logo</a>';
}
clubInfo += ' | Match: ' + matchType + ' (' + confidence + ')';
clubInfo += '</small>';
}
infoElement.innerHTML = '<strong>Next Match:</strong> ' +
data.date_formatted + ' vs ' + data.opponent +
' (' + (data.is_home ? 'Home' : 'Away') + ' - ' + data.venue + ')' +
'<br><small>Division: ' + data.division + ' | Time: ' + data.time + '</small>' +
clubInfo;
infoElement.style.display = 'block';
statusElement.innerHTML = '<span class="text-success">✓ Fixture loaded!</span>';
// Clear status message after 3 seconds
setTimeout(function() {
statusElement.innerHTML = '';
}, 3000);
} else {
statusElement.innerHTML = '<span class="text-danger">✗ ' + data.message + '</span>';
infoElement.style.display = 'none';
}
loadBtn.disabled = false;
})
.catch(error => {
console.error('Error:', error);
statusElement.innerHTML = '<span class="text-danger">✗ Error loading fixture</span>';
infoElement.style.display = 'none';
loadBtn.disabled = false;
});
}
// Auto-load fixture on page load
function myFunction() {
// Optional: Auto-load the next fixture when the page loads
// Uncomment the next line if you want this behavior
// loadNextFixture();
}
</script>
</body>
</html>

View File

@ -1,272 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MOTM Management - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.fixture-header {
background-color: #f8f9fa;
font-weight: bold;
}
.reset-section {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 2rem;
}
.data-section {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 2rem;
}
</style>
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<h1>MOTM/DotD Management</h1>
<p class="lead">Manage and reset Man of the Match and Dick of the Day counts</p>
<div class="mb-3">
<a href="/admin" class="btn btn-outline-primary">Back to Admin Dashboard</a>
<a href="/admin/motm" class="btn btn-outline-secondary">MOTM Admin</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Reset Controls -->
<div class="reset-section">
<h3>Reset Controls</h3>
<p class="text-muted">Use these controls to reset MOTM/DotD counts for specific fixtures or all data.</p>
<div class="row">
<div class="col-md-4">
<h5>Reset Specific Fixture</h5>
<form method="POST" onsubmit="return confirm('Are you sure you want to reset MOTM/DotD counts for this fixture?')">
<input type="hidden" name="action" value="reset_fixture">
<div class="mb-3">
<select name="fixture_date" class="form-select" required>
<option value="">Select fixture date...</option>
{% for date in fixture_dates %}
<option value="{{ date }}">{{ date }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-warning">Reset Fixture</button>
</form>
</div>
<div class="col-md-4">
<h5>Reset All Totals</h5>
<p class="text-muted">Reset motmtotal, dotdtotal, assiststotal, goalstotal columns</p>
<form method="POST" onsubmit="return confirm('Are you sure you want to reset all MOTM/DotD totals? This will affect the Player of the Year calculations.')">
<input type="hidden" name="action" value="reset_totals">
<button type="submit" class="btn btn-danger">Reset All Totals</button>
</form>
</div>
<div class="col-md-4">
<h5>Reset Everything</h5>
<p class="text-muted">Reset all MOTM/DotD data including fixture-specific columns</p>
<form method="POST" onsubmit="return confirm('Are you sure you want to reset ALL MOTM/DotD data? This action cannot be undone.')">
<input type="hidden" name="action" value="reset_all">
<button type="submit" class="btn btn-danger">Reset Everything</button>
</form>
</div>
</div>
<div class="row mt-3">
<div class="col-md-4">
<h5>Sync Totals</h5>
<p class="text-muted">Update stored totals to match calculated values from fixture columns</p>
<form method="POST" onsubmit="return confirm('Sync stored totals with calculated values?')">
<input type="hidden" name="action" value="sync_totals">
<button type="submit" class="btn btn-info">Sync Totals</button>
</form>
</div>
</div>
</div>
<!-- Current Data Display -->
<div class="data-section">
<h3>Current MOTM/DotD Data</h3>
{% if motm_data %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Player #</th>
<th>Player Name</th>
<th>MOTM Total</th>
<th>DotD Total</th>
<th>Goals Total</th>
<th>Assists Total</th>
{% for date in fixture_dates %}
<th class="fixture-header">MOTM {{ date }}</th>
<th class="fixture-header">DotD {{ date }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for player in motm_data %}
<tr>
<td>{{ player.playernumber }}</td>
<td>{{ player.playername }}</td>
<td>
<span class="badge bg-primary">{{ player.calculated_motmtotal or 0 }}</span>
{% if player.motmtotal != player.calculated_motmtotal %}
<small class="text-warning">(stored: {{ player.motmtotal or 0 }})</small>
{% endif %}
</td>
<td>
<span class="badge bg-secondary">{{ player.calculated_dotdtotal or 0 }}</span>
{% if player.dotdtotal != player.calculated_dotdtotal %}
<small class="text-warning">(stored: {{ player.dotdtotal or 0 }})</small>
{% endif %}
</td>
<td>
<span class="badge bg-success">{{ player.goalstotal or 0 }}</span>
</td>
<td>
<span class="badge bg-info">{{ player.assiststotal or 0 }}</span>
</td>
{% for date in fixture_dates %}
<td>
{% set motm_col = 'motm_' + date %}
{% set dotd_col = 'dotd_' + date %}
{% if player[motm_col] and player[motm_col] > 0 %}
<span class="badge bg-primary">{{ player[motm_col] }}</span>
<button type="button" class="btn btn-sm btn-outline-danger ms-1"
onclick="resetPlayerFixture({{ player.playernumber }}, '{{ date }}')"
title="Reset this player's counts for {{ date }}">
×
</button>
{% else %}
<span class="text-muted">0</span>
{% endif %}
</td>
<td>
{% if player[dotd_col] and player[dotd_col] > 0 %}
<span class="badge bg-secondary">{{ player[dotd_col] }}</span>
<button type="button" class="btn btn-sm btn-outline-danger ms-1"
onclick="resetPlayerFixture({{ player.playernumber }}, '{{ date }}')"
title="Reset this player's counts for {{ date }}">
×
</button>
{% else %}
<span class="text-muted">0</span>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<h5>No MOTM/DotD data found</h5>
<p>There is no data in the _hkfc_c_motm table. This might be because no votes have been cast yet.</p>
</div>
{% endif %}
</div>
<!-- Column Management -->
<div class="reset-section">
<h3>Column Management</h3>
<p class="text-muted">Drop unwanted columns from the _hkfc_c_motm table.</p>
<div class="row">
<div class="col-md-6">
<h5>Drop Specific Column</h5>
<form method="POST" onsubmit="return confirm('Are you sure you want to drop this column? This action cannot be undone!')">
<input type="hidden" name="action" value="drop_column">
<div class="mb-3">
<select name="column_name" class="form-select" required>
<option value="">Select column to drop...</option>
{% for date in fixture_dates %}
<option value="motm_{{ date }}">motm_{{ date }}</option>
<option value="dotd_{{ date }}">dotd_{{ date }}</option>
{% endfor %}
<option value="motm_none">motm_none</option>
<option value="dotd_none">dotd_none</option>
</select>
</div>
<button type="submit" class="btn btn-danger">Drop Column</button>
</form>
</div>
<div class="col-md-6">
<h5>Drop Fixture Columns</h5>
<form method="POST" onsubmit="return confirm('Are you sure you want to drop all columns for this fixture? This action cannot be undone!')">
<input type="hidden" name="action" value="drop_fixture_columns">
<div class="mb-3">
<select name="fixture_date" class="form-select" required>
<option value="">Select fixture date...</option>
{% for date in fixture_dates %}
<option value="{{ date }}">{{ date }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-danger">Drop Fixture Columns</button>
</form>
</div>
</div>
</div>
<!-- Available Fixtures -->
<div class="data-section">
<h3>Available Fixtures</h3>
{% if fixture_dates %}
<p>The following fixture dates have MOTM/DotD columns in the database:</p>
<ul>
{% for date in fixture_dates %}
<li><code>{{ date }}</code> - Columns: <code>motm_{{ date }}</code>, <code>dotd_{{ date }}</code></li>
{% endfor %}
</ul>
{% else %}
<div class="alert alert-warning">
<h5>No fixture columns found</h5>
<p>No fixture-specific MOTM/DotD columns were found in the database.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Hidden form for individual player resets -->
<form id="playerResetForm" method="POST" style="display: none;">
<input type="hidden" name="action" value="reset_player_fixture">
<input type="hidden" name="player_number" id="playerNumber">
<input type="hidden" name="fixture_date" id="fixtureDate">
</form>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
function resetPlayerFixture(playerNumber, fixtureDate) {
if (confirm(`Are you sure you want to reset MOTM/DotD counts for player #${playerNumber} in fixture ${fixtureDate}?`)) {
document.getElementById('playerNumber').value = playerNumber;
document.getElementById('fixtureDate').value = fixtureDate;
document.getElementById('playerResetForm').submit();
}
}
</script>
</body>
</html>

View File

@ -1,83 +0,0 @@
<html>
<head>
<title>HKFC Men's C Team - MotM and DotD online vote</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<h3>HKFC Men's C Team MotM and DotD online vote</h3>
<h5>{{ formatDate }}</h5>
<h4><img src="{{ hkfcLogo }}" height="150"></img> <b> </b> <img src="{{ oppoLogo }}" height="140"></img></h4>
<body>
<p><b>Randomly selected comment from the match:</b>
<br/>
{% for item in comment %}
<i>{{ item.comment }}</i>
{% endfor %}
</p>
<dl>
{{ form.csrf_token }}
<div class="row">
<div class="col-xs-12">
<form class="col-sm-6" method="post" action="/motm/vote-thanks" id="motmForm" accept-charset="utf-8">
<input type="hidden" id="matchNumber" name="matchNumber" value="{{ matchNumber }}">
<input type="hidden" id="oppo" name="oppo" value="{{ oppo }}">
<div class = "row">
<div class = "col-sm-6">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Man of the Match</span>
<select class="form-control" name="motmVote" required>
{% for item in data %}
{% if item.playernickname != "" %}
<option value={{ item.playernumber }}>{{ item.playernickname }}</option>
{% else %}
<option value={{ item.playernumber }}>{{ item.playersurname }}, {{ item.playerforenames }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class = "col-sm-6">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Dick of the Day</span>
<select class="form-control" name="dotdVote" required>
{% for item in data %}
{% if item.playernickname != "" %}
<option value={{ item.playernumber }}>{{ item.playernickname }}</option>
{% else %}
<option value={{ item.playernumber }}>{{ item.playersurname }}, {{ item.playerforenames }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
</div>
<div class = "row">
<div class = "col-sm-6">
<div class = "input-group">
<span class = "input-group-addon" id = "basic-addon1">Match comments</span>
<textarea rows = "4" cols = "80" name = "motmComment" form = "motmForm">Optional comments added here</textarea>
</div>
</div>
</div>
<div class = "row">
<h3>Rogues Gallery</h3>
<div class = "col-sm-4">
<h4>Current Man of the Match</h4>
<img src="{{ motmURL }}" height="200"></img>
</div>
<div class = "col-sm-4">
<h4>Current Dick of the Day</h4>
<img src="{{ dotdURL }}" height="200"></img>
</div>
</div>
<button type="submit" class="btn btn-success">Submit</button>
<a class="btn btn-danger" href="/" role="button">Cancel</a>
</form>
</div>
</div>
</dl>
</body>
</html>

View File

@ -1,87 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Player Management - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<h1>Player Management</h1>
<p class="lead">Manage players in the HKFC Men's C Team database</p>
<div class="mb-3">
<a href="/admin/players/add" class="btn btn-primary">Add New Player</a>
<a href="/admin" class="btn btn-secondary">Back to Admin</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="card">
<div class="card-header">
<h5>All Players</h5>
</div>
<div class="card-body">
{% if players %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Number</th>
<th>First Names</th>
<th>Surname</th>
<th>Nickname</th>
<th>Team</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for player in players %}
<tr>
<td>{{ player.playernumber }}</td>
<td>{{ player.playerforenames }}</td>
<td>{{ player.playersurname }}</td>
<td>{{ player.playernickname }}</td>
<td>
<span class="badge bg-{{ 'primary' if player.playerteam == 'HKFC C' else 'secondary' }}">
{{ player.playerteam }}
</span>
</td>
<td>
<a href="/admin/players/edit/{{ player.playernumber }}" class="btn btn-sm btn-outline-primary">Edit</a>
<form method="POST" action="/admin/players/delete/{{ player.playernumber }}" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this player?')">
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<h5>No players found</h5>
<p>There are no players in the database. <a href="/admin/players/add">Add the first player</a> to get started.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,19 +0,0 @@
<html>
<head>
<title>Player Removed</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h3>Player {{ number }} has been removed from the squad</h3>
<a class="btn btn-primary" href="/admin/squad/list" role="button">Back to Squad List</a>
<a class="btn btn-danger" href="/" role="button">Home</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,56 +0,0 @@
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="chart_div" style="width: 800px; height: 1000px;"></div>
<script type="text/javascript">
google.charts.load('current', {
packages: ['corechart']
}).then(function () {
// create chart
var container = $('#chart_div').get(0);
var chart = new google.visualization.ColumnChart(container);
var options = {
legend: {
position: 'top'
}
};
// create data table
var data = new google.visualization.DataTable();
data.addColumn('string', 'Player');
data.addColumn('number', 'MotM Total');
data.addColumn('number', 'DotD Total');
// get data
$.ajax({
url: '/api/poty-results',
dataType: 'json'
}).done(function (jsonData) {
loadData(jsonData);
}).fail(function (jqXHR, textStatus, errorThrown) {
var jsonData = [{"motmTotal": 5, "playerName": "ERVINE Jonathan Desmond", "dotdTotal": 2}, {"motmTotal": 3, "playerName": "MCDONAGH Jerome Michael", "dotdTotal": 1}];
loadData(jsonData);
});
// load json data
function loadData(jsonData) {
$.each(jsonData, function(index, row) {
data.addRow([
row.playerName,
row.motmTotal,
row.dotdTotal
]);
});
drawChart();
}
// draw chart
$(window).resize(drawChart);
function drawChart() {
chart.draw(data, options);
}
});
</script>

View File

@ -1,255 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>S3 Configuration - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.config-section {
background-color: #f8f9fa;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1rem;
}
.help-text {
font-size: 0.875rem;
color: #6c757d;
}
</style>
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<h1>S3 Configuration</h1>
<p class="lead">Configure AWS S3 storage for logos and assets</p>
<div class="mb-3">
<a href="/admin" class="btn btn-secondary">Back to Admin</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
{{ form.hidden_tag() }}
<!-- Enable S3 Section -->
<div class="config-section">
<h4>Storage Configuration</h4>
<div class="form-check form-switch mb-3">
{{ form.enable_s3(class="form-check-input") }}
{{ form.enable_s3.label(class="form-check-label") }}
<div class="help-text">When enabled, logos and assets will be served from S3. When disabled, local static files will be used.</div>
</div>
<div class="form-check form-switch mb-3">
{{ form.fallback_to_static(class="form-check-input") }}
{{ form.fallback_to_static.label(class="form-check-label") }}
<div class="help-text">If S3 is unavailable, fallback to local static files.</div>
</div>
</div>
<!-- Storage Provider Section -->
<div class="config-section">
<h4>Storage Provider</h4>
<div class="mb-3">
{{ form.storage_provider.label(class="form-label") }}
{{ form.storage_provider(class="form-select") }}
<div class="help-text">Choose between AWS S3 or MinIO (self-hosted S3-compatible storage).</div>
</div>
</div>
<!-- Credentials Section -->
<div class="config-section">
<h4>Access Credentials</h4>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
{{ form.aws_access_key_id.label(class="form-label") }}
{{ form.aws_access_key_id(class="form-control", placeholder="AKIA...") }}
<div class="help-text">Your Access Key ID (AWS or MinIO)</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
{{ form.aws_secret_access_key.label(class="form-label") }}
{{ form.aws_secret_access_key(class="form-control", placeholder="Enter secret key") }}
<div class="help-text">Your Secret Access Key (AWS or MinIO)</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
{{ form.aws_region.label(class="form-label") }}
{{ form.aws_region(class="form-control") }}
<div class="help-text">Region (AWS) or leave default for MinIO</div>
</div>
</div>
</div>
</div>
<!-- MinIO Configuration Section -->
<div class="config-section" id="minio-config" style="display: none;">
<h4>MinIO Configuration</h4>
<div class="row">
<div class="col-md-8">
<div class="mb-3">
{{ form.minio_endpoint.label(class="form-label") }}
{{ form.minio_endpoint(class="form-control", placeholder="minio.example.com:9000") }}
<div class="help-text">MinIO server endpoint (hostname:port)</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<div class="form-check form-switch">
{{ form.minio_use_ssl(class="form-check-input") }}
{{ form.minio_use_ssl.label(class="form-check-label") }}
</div>
<div class="help-text">Enable SSL/TLS for MinIO connection</div>
</div>
</div>
</div>
</div>
<!-- S3 Bucket Configuration -->
<div class="config-section">
<h4>S3 Bucket Configuration</h4>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
{{ form.bucket_name.label(class="form-label") }}
{{ form.bucket_name(class="form-control", placeholder="my-motm-assets") }}
<div class="help-text">Name of your S3 bucket</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
{{ form.bucket_prefix.label(class="form-label") }}
{{ form.bucket_prefix(class="form-control") }}
<div class="help-text">Prefix for objects in the bucket (e.g., motm-assets/)</div>
</div>
</div>
</div>
</div>
<!-- URL Configuration -->
<div class="config-section">
<h4>URL Configuration</h4>
<div class="form-check form-switch mb-3">
{{ form.use_signed_urls(class="form-check-input") }}
{{ form.use_signed_urls.label(class="form-check-label") }}
<div class="help-text">Use signed URLs for secure access to private objects. If disabled, objects must be public.</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
{{ form.signed_url_expiry.label(class="form-label") }}
{{ form.signed_url_expiry(class="form-control") }}
<div class="help-text">How long signed URLs remain valid (in seconds)</div>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="d-flex gap-2">
{{ form.test_connection(class="btn btn-info") }}
{{ form.save_config(class="btn btn-primary") }}
{{ form.cancel(class="btn btn-secondary") }}
</div>
</form>
<!-- Current Configuration Display -->
<div class="config-section mt-4">
<h4>Current Configuration</h4>
<div class="row">
<div class="col-md-6">
<strong>S3 Status:</strong>
<span class="badge bg-{{ 'success' if current_config.get('enable_s3') else 'secondary' }}">
{{ 'Enabled' if current_config.get('enable_s3') else 'Disabled' }}
</span>
</div>
<div class="col-md-6">
<strong>Bucket:</strong> {{ current_config.get('bucket_name', 'Not configured') }}
</div>
</div>
<div class="row mt-2">
<div class="col-md-6">
<strong>Region:</strong> {{ current_config.get('aws_region', 'Not configured') }}
</div>
<div class="col-md-6">
<strong>Signed URLs:</strong>
<span class="badge bg-{{ 'success' if current_config.get('use_signed_urls') else 'secondary' }}">
{{ 'Enabled' if current_config.get('use_signed_urls') else 'Disabled' }}
</span>
</div>
</div>
</div>
<!-- Help Section -->
<div class="config-section mt-4">
<h4>Setup Instructions</h4>
<ol>
<li><strong>Create S3 Bucket:</strong> Create an S3 bucket in your AWS account</li>
<li><strong>Set Permissions:</strong> Configure bucket permissions for your access key</li>
<li><strong>Upload Assets:</strong> Upload your logos and assets to the bucket</li>
<li><strong>Configure Here:</strong> Enter your credentials and bucket details above</li>
<li><strong>Test Connection:</strong> Use the "Test S3 Connection" button to verify</li>
<li><strong>Save Configuration:</strong> Save your settings to enable S3 storage</li>
</ol>
<div class="alert alert-info">
<h6>Asset Organization</h6>
<p>Organize your assets in the S3 bucket as follows:</p>
<ul class="mb-0">
<li><code>assets/images/clubs/</code> - Club logos</li>
<li><code>assets/images/</code> - General images</li>
<li><code>assets/logos/</code> - Alternative logo location</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Show/hide MinIO configuration based on storage provider selection
function toggleMinioConfig() {
const storageProvider = document.getElementById('storage_provider');
const minioConfig = document.getElementById('minio-config');
if (storageProvider.value === 'minio') {
minioConfig.style.display = 'block';
} else {
minioConfig.style.display = 'none';
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
toggleMinioConfig();
// Add event listener for storage provider changes
const storageProviderSelect = document.getElementById('storage_provider');
if (storageProviderSelect) {
storageProviderSelect.addEventListener('change', toggleMinioConfig);
}
});
</script>
</body>
</html>

View File

@ -1,154 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>S3 Status - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<h1>S3 Storage Status</h1>
<p class="lead">Current S3 configuration and connection status</p>
<div class="mb-3">
<a href="/admin/s3-config" class="btn btn-primary">Configure S3</a>
<a href="/admin" class="btn btn-secondary">Back to Admin</a>
</div>
<div class="card">
<div class="card-header">
<h5>Configuration Status</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<table class="table table-borderless">
<tr>
<td><strong>S3 Storage:</strong></td>
<td>
<span class="badge bg-{{ 'success' if s3_info.enabled else 'secondary' }}">
{{ 'Enabled' if s3_info.enabled else 'Disabled' }}
</span>
</td>
</tr>
<tr>
<td><strong>Provider:</strong></td>
<td>{{ s3_info.storage_provider.title() if s3_info.get('storage_provider') else 'AWS S3' }}</td>
</tr>
<tr>
<td><strong>Bucket Name:</strong></td>
<td>{{ s3_info.bucket_name }}</td>
</tr>
<tr>
<td><strong>Region:</strong></td>
<td>{{ s3_info.aws_region }}</td>
</tr>
{% if s3_info.get('storage_provider') == 'minio' %}
<tr>
<td><strong>MinIO Endpoint:</strong></td>
<td>{{ s3_info.get('minio_endpoint', 'Not configured') }}</td>
</tr>
{% endif %}
</table>
</div>
<div class="col-md-6">
<table class="table table-borderless">
<tr>
<td><strong>Signed URLs:</strong></td>
<td>
<span class="badge bg-{{ 'success' if s3_info.use_signed_urls else 'secondary' }}">
{{ 'Enabled' if s3_info.use_signed_urls else 'Disabled' }}
</span>
</td>
</tr>
<tr>
<td><strong>Fallback:</strong></td>
<td>
<span class="badge bg-{{ 'success' if s3_info.fallback_enabled else 'warning' }}">
{{ 'Enabled' if s3_info.fallback_enabled else 'Disabled' }}
</span>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h5>Connection Status</h5>
</div>
<div class="card-body">
<div class="alert alert-{{ 'success' if s3_info.connection_success else 'danger' }}" role="alert">
<h6 class="alert-heading">
{{ '✅ Connection Successful' if s3_info.connection_success else '❌ Connection Failed' }}
</h6>
<p class="mb-0">{{ s3_info.connection_status }}</p>
</div>
</div>
</div>
{% if s3_info.enabled and s3_info.connection_success %}
<div class="card mt-4">
<div class="card-header">
<h5>Asset Management</h5>
</div>
<div class="card-body">
<p>S3 storage is properly configured and connected. Assets will be served from S3.</p>
<div class="alert alert-info">
<h6>Asset Locations</h6>
<ul class="mb-0">
<li><strong>Club Logos:</strong> <code>{{ s3_info.bucket_name }}/assets/images/clubs/</code></li>
<li><strong>General Images:</strong> <code>{{ s3_info.bucket_name }}/assets/images/</code></li>
<li><strong>Player Images:</strong> <code>{{ s3_info.bucket_name }}/assets/images/players/</code></li>
</ul>
</div>
</div>
</div>
{% elif s3_info.enabled and not s3_info.connection_success %}
<div class="card mt-4">
<div class="card-header">
<h5>Troubleshooting</h5>
</div>
<div class="card-body">
<div class="alert alert-warning">
<h6>S3 is enabled but connection failed</h6>
<p>Check the following:</p>
<ul>
<li>Verify AWS credentials are correct</li>
<li>Ensure the bucket name exists</li>
<li>Check bucket permissions</li>
<li>Verify AWS region is correct</li>
</ul>
</div>
</div>
</div>
{% else %}
<div class="card mt-4">
<div class="card-header">
<h5>Static File Storage</h5>
</div>
<div class="card-body">
<p>S3 storage is disabled. Assets are being served from local static files.</p>
<div class="alert alert-info">
<h6>Local Asset Locations</h6>
<ul class="mb-0">
<li><strong>Club Logos:</strong> <code>/static/images/clubs/</code></li>
<li><strong>General Images:</strong> <code>/static/images/</code></li>
<li><strong>Player Images:</strong> <code>/static/images/players/</code></li>
</ul>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,83 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Team Management - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<h1>Team Management</h1>
<p class="lead">Manage hockey teams in the database</p>
<div class="mb-3">
<a href="/admin/teams/add" class="btn btn-primary">Add New Team</a>
<a href="/admin" class="btn btn-secondary">Back to Admin</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="card">
<div class="card-header">
<h5>All Teams</h5>
</div>
<div class="card-body">
{% if teams %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>Club</th>
<th>Team</th>
<th>Display Name</th>
<th>League</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr>
<td>{{ team.id }}</td>
<td>{{ team.club }}</td>
<td>{{ team.team }}</td>
<td>{{ team.display_name }}</td>
<td>{{ team.league }}</td>
<td>
<a href="/admin/teams/edit/{{ team.id }}" class="btn btn-sm btn-outline-primary">Edit</a>
<form method="POST" action="/admin/teams/delete/{{ team.id }}" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this team?')">
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<h5>No teams found</h5>
<p>There are no teams in the database. <a href="/admin/teams/add">Add the first team</a> to get started.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,56 +0,0 @@
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="chart_div" style="width: 800px; height: 1000px;"></div>
<script type="text/javascript">
google.charts.load('current', {
packages: ['corechart']
}).then(function () {
// create chart
var container = $('#chart_div').get(0);
var chart = new google.visualization.ColumnChart(container);
var options = {
legend: {
position: 'top'
}
};
// create data table
var data = new google.visualization.DataTable();
data.addColumn('string', 'Player');
data.addColumn('number', 'MotM');
data.addColumn('number', 'DotD');
// get data
$.ajax({
url: '/api/vote-results',
dataType: 'json'
}).done(function (jsonData) {
loadData(jsonData);
}).fail(function (jqXHR, textStatus, errorThrown) {
var jsonData = [{"motm_{{ _matchDate }}": 1, "playerName": "ERVINE Jonathan Desmond", "dotd_{{ _matchDate }}": 0}, {"motm_{{ _matchDate }}": 0, "playerName": "MCDONAGH Jerome Michael", "dotd_{{ _matchDate }}": 1}];
loadData(jsonData);
});
// load json data
function loadData(jsonData) {
$.each(jsonData, function(index, row) {
data.addRow([
row.playerName,
row.motm_{{ _matchDate }},
row.dotd_{{ _matchDate }}
]);
});
drawChart();
}
// draw chart
$(window).resize(drawChart);
function drawChart() {
chart.draw(data, options);
}
});
</script>

View File

@ -1,19 +0,0 @@
<html>
<head>
<title>HKFC Men's C Team - MotM and DotD vote</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<h2>Thanks for submitting the MotM and DotD votes</h2>
<body>
Smithers' army of Internet monkeys will now go about adding up the votes ...
<p>
<img src="http://icarus.ipa.champion:9000/hockey-app/assets/simpsons-monkeys.jpg"></img>
</p>
<a class="btn btn-primary" href="/" role="button">Home</a>
<a class="btn btn-info" href="/motm/comments" role="button">Comments</a>
</body>
</html>

View File

@ -1,41 +0,0 @@
#!/usr/bin/env python3
"""
Simple test script to verify the MOTM Flask application can start without errors.
"""
import sys
import os
# Add the current directory to Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
from main import app
print("✓ MOTM Flask application imported successfully")
# Test that we can create the app context
with app.app_context():
print("✓ Flask application context created successfully")
# Test that routes are registered
routes = [rule.rule for rule in app.url_map.iter_rules()]
print(f"✓ Found {len(routes)} registered routes")
# Check for key routes
key_routes = ['/', '/motm/', '/admin/motm', '/admin/squad']
for route in key_routes:
if any(route in r for r in routes):
print(f"✓ Key route {route} is registered")
else:
print(f"⚠ Key route {route} not found")
print("\n🎉 MOTM application test completed successfully!")
print("You can now run 'python main.py' to start the application.")
except ImportError as e:
print(f"❌ Import error: {e}")
print("Make sure all dependencies are installed: pip install -r requirements.txt")
sys.exit(1)
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)

View File

@ -1,24 +1,11 @@
Flask>=2.0.0,<3.0.0
Werkzeug>=2.0.0
Flask
Werkzeug
email-validator
flask_table
flask-mysql
flask_login
Flask-BasicAuth
Flask-Bootstrap
flask_wtf
wtforms>=3.0.0
wtforms_components
MarkupSafe>=2.0.0
# SQLAlchemy and database drivers
SQLAlchemy>=2.0.0
Flask-SQLAlchemy>=3.0.0
alembic>=1.12.0
# Database drivers
pymysql>=1.1.0
psycopg2-binary>=2.9.0
PyMySQL>=1.1.0
# Legacy support (can be removed after migration)
flask-mysql

View File

@ -3,4 +3,5 @@ routes = Blueprint('routes', __name__)
from .dashboard import *
from ._matches import *
from ._hkfcD_motm import *
from ._convenor import *

View File

@ -148,7 +148,7 @@ def hkfcDMotmAdmin():
currSuffix = tempSuffix[0]['motmUrlSuffix']
print(currSuffix)
flash('Man of the Match vote is now activated')
flash('MotM URL https://hockey.ervine.cloud/hkfc-d/motm/'+currSuffix)
flash('MotM URL https://hockey.ppspone.cloud/hkfc-d/motm/'+currSuffix)
else:
flash('Something went wrong - check with Smithers')

View File

@ -68,6 +68,12 @@ class convenorSquadListTable(Table):
edit = ButtonCol('Edit', 'routes.convenorEditPlayer', url_kwargs=dict(playerNumber='playerNumber'), button_attrs={"type" : "submit", "class" : "btn btn-primary"})
delete = ButtonCol('Delete', 'routes.convenorDeletePlayer', url_kwargs=dict(playerNumber='playerNumber'), button_attrs={"type" : "submit", "class" : "btn btn-danger"})
class matchSquadTable(Table):
playerNumber = Col('Player Number')
playerNickname = Col('Nickname')
playerSurname = Col('Surname')
playerForenames = Col('Forenames')
delete = ButtonCol('Delete', 'routes.delPlayerFromSquad', url_kwargs=dict(playerNumber='playerNumber'), button_attrs={"type" : "submit", "class" : "btn btn-danger"})
class convenorFixtureList(Table):
date = Col('Date')

Some files were not shown because too many files have changed in this diff Show More