Compare commits
1 Commits
master
...
migrate-to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d0be883da |
133
.dockerignore
133
.dockerignore
@ -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
|
||||
@ -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
362
DOCKER.md
@ -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/)
|
||||
1
app.yaml
1
app.yaml
@ -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
|
||||
|
||||
16
dbWrite.py
16
dbWrite.py
@ -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()
|
||||
|
||||
@ -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
|
||||
127
docker/build.sh
127
docker/build.sh
@ -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}"
|
||||
192
docker/run.sh
192
docker/run.sh
@ -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
|
||||
@ -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
|
||||
54
forms.py
54
forms.py
@ -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
17
main.py
@ -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>'
|
||||
|
||||
@ -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
150
motm_app/.gitignore
vendored
@ -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
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
@ -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"]
|
||||
@ -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.
|
||||
@ -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
|
||||
@ -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.
|
||||
|
||||
@ -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! 🎉
|
||||
|
||||
@ -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
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
@ -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))
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
]
|
||||
@ -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()
|
||||
@ -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")
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.")
|
||||
|
||||
@ -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')
|
||||
@ -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
|
||||
|
||||
|
||||
2039
motm_app/main.py
2039
motm_app/main.py
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"enable_s3": true,
|
||||
"storage_provider": "minio",
|
||||
"aws_access_key_id": "AKIARLJ7D6ZPRRLQHWD7",
|
||||
"aws_secret_access_key": "Ih8C5I8z7Or/+JGMzT0Pqjuqm7ig9Qells8qsd8q",
|
||||
"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
|
||||
}
|
||||
@ -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)
|
||||
@ -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"
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
6
motm_app/static/css/bootstrap-theme.min.css
vendored
6
motm_app/static/css/bootstrap-theme.min.css
vendored
File diff suppressed because one or more lines are too long
6
motm_app/static/css/bootstrap.min.css
vendored
6
motm_app/static/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
@ -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 |
@ -1 +0,0 @@
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
7
motm_app/static/js/bootstrap.min.js
vendored
7
motm_app/static/js/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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)
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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">×</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 %}
|
||||
@ -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 %}
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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)
|
||||
@ -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
|
||||
|
||||
|
||||
@ -3,4 +3,5 @@ routes = Blueprint('routes', __name__)
|
||||
|
||||
from .dashboard import *
|
||||
from ._matches import *
|
||||
from ._hkfcD_motm import *
|
||||
from ._convenor import *
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user