Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8d5c7dee4 | ||
|
|
7545f0cdce | ||
|
|
4f9d54e855 | ||
|
|
4d04ce697f | ||
|
|
ec7ff3e3e1 | ||
|
|
0ad23e9be4 | ||
|
|
0628289681 | ||
|
|
b7f11ea34b | ||
|
|
fad4932de5 | ||
|
|
5defae4f51 | ||
|
|
58cd67cc1a | ||
|
|
a2961e05c5 | ||
|
|
c7c7c9f215 | ||
|
|
2b6cbbed85 | ||
|
|
1c1b0e6f05 | ||
|
|
5f1e4eedc8 | ||
|
|
1ad9c9e496 | ||
|
|
11cba3f138 | ||
|
|
b4a12a8a91 | ||
|
|
0fba68287e | ||
|
|
015f576429 | ||
|
|
d7ce602fb0 | ||
|
|
57605fce0e | ||
|
|
f9aa709a4b | ||
|
|
b790b733ea | ||
|
|
8a834b9424 | ||
|
|
8c9b95a723 | ||
|
|
ac91871c3a | ||
|
|
e5337a204b | ||
|
|
734ae1c7e6 | ||
|
|
ec6f4d0793 | ||
|
|
d6beb7a040 |
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/)
|
||||
11
dbWrite.py
11
dbWrite.py
@ -6,7 +6,6 @@ import json
|
||||
# These environment variables are configured in app.yaml.
|
||||
|
||||
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"
|
||||
@ -14,7 +13,7 @@ CLOUDSQL_PASSWORD = "P8P1YopMlwg8TxhE"
|
||||
CLOUDSQL_WRITE_PASSWORD = "1URYcxXXlQ6xOWgj"
|
||||
CLOUDSQL_READ_PASSWORD = "o4GWrbbkBKy3oR6u"
|
||||
CLOUDSQL_DATABASE = "20209_hockeyResults"
|
||||
LOCAL_DATABASE = "hockeyResults2021"
|
||||
LOCAL_DATABASE = "hockeyResults2020"
|
||||
CLOUDSQL_DATABASE_STATIC = "hockeyResults"
|
||||
CLOUDSQL_CHARSET = "utf8"
|
||||
|
||||
@ -38,7 +37,7 @@ def write_cloudsql():
|
||||
#
|
||||
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)
|
||||
db = pymysql.connect(host='db.ipa.champion', user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=LOCAL_DATABASE, charset=CLOUDSQL_CHARSET)
|
||||
return db
|
||||
|
||||
def write_cloudsql_static():
|
||||
@ -60,7 +59,7 @@ def write_cloudsql_static():
|
||||
#
|
||||
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)
|
||||
db = pymysql.connect(host='db.ipa.champion', user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
|
||||
return db
|
||||
|
||||
def read_cloudsql():
|
||||
@ -82,7 +81,7 @@ def read_cloudsql():
|
||||
#
|
||||
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)
|
||||
db = pymysql.connect(host='db.ipa.champion', user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=LOCAL_DATABASE, charset=CLOUDSQL_CHARSET)
|
||||
return db
|
||||
|
||||
def read_cloudsql_static():
|
||||
@ -104,7 +103,7 @@ def read_cloudsql_static():
|
||||
#
|
||||
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)
|
||||
db = pymysql.connect(host='db.ipa.champion', user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
|
||||
return db
|
||||
|
||||
def sql_write(sql_cmd):
|
||||
|
||||
@ -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
|
||||
62
forms.py
62
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):
|
||||
@ -53,7 +54,7 @@ class playerDbCreateForm(FlaskForm):
|
||||
|
||||
class searchForm(FlaskForm):
|
||||
seasonStart = datetime.strptime('2018-09-01', '%Y-%m-%d')
|
||||
season = SelectField('Season data to search', choices=[('2021', '2021/22'), ('2020', '2020/21'), ('2019', '2019/20'), ('2018', '2018/19'), ('2017', '2017/18'), ('2016', '2016/17'), ('2015', '2015/16'), ('2014', '2014/15'), ('2013', '2013/14')])
|
||||
season = SelectField('Season data to search', choices=[('2020', '2020/21'), ('2019', '2019/20'), ('2018', '2018/19'), ('2017', '2017/18'), ('2016', '2016/17'), ('2015', '2015/16'), ('2014', '2014/15'), ('2013', '2013/14')])
|
||||
clubName = SelectField("Club to search", choices=[], coerce=str)
|
||||
teamName = SelectField("Select a Team", choices=[])
|
||||
startDate = DateField('DatePicker', format='%Y-%m-%d', default=seasonStart)
|
||||
@ -61,22 +62,29 @@ class searchForm(FlaskForm):
|
||||
submitButton = SubmitField("Submit")
|
||||
|
||||
class playerRecordsForm(FlaskForm):
|
||||
season = SelectField('Season data to search', choices=[('2021', '2021/22'), ('2020', '2020/21'), ('2019', '2019/20'), ('2018', '2018/19'), ('2017', '2017/18'), ('2016', '2016/17'), ('2015', '2015/16'), ('2014', '2014/15'), ('2013', '2013/14')])
|
||||
season = SelectField('Season data to search', choices=[('2020', '2020/21'), ('2019', '2019/20'), ('2018', '2018/19'), ('2017', '2017/18'), ('2016', '2016/17'), ('2015', '2015/16'), ('2014', '2014/15'), ('2013', '2013/14')])
|
||||
clubName = SelectField("Club to search", choices=[], coerce=str)
|
||||
teamName = SelectField("Select a Team", choices=[])
|
||||
submitButton = SubmitField("Submit")
|
||||
|
||||
class teamRecordsForm(FlaskForm):
|
||||
season = SelectField('Season data to search', choices=[('2021', '2021/22'), ('2020', '2020/21'), ('2019', '2019/20'), ('2018', '2018/19'), ('2017', '2017/18'), ('2016', '2016/17'), ('2015', '2015/16'), ('2014', '2014/15'), ('2013', '2013/14')])
|
||||
season = SelectField('Season data to search', choices=[('2020', '2020/21'), ('2019', '2019/20'), ('2018', '2018/19'), ('2017', '2017/18'), ('2016', '2016/17'), ('2015', '2015/16'), ('2014', '2014/15'), ('2013', '2013/14')])
|
||||
clubName = SelectField("Club to search", choices=[], coerce=str)
|
||||
teamName = SelectField("Select a Team", choices=[])
|
||||
submitButton = SubmitField("Submit")
|
||||
|
||||
class clubPlayingRecordsForm(FlaskForm):
|
||||
season = SelectField('Season data to search', choices=[('2021', '2021/22'), ('2020', '2020/21'), ('2019', '2019/20'), ('2018', '2018/19'), ('2017', '2017/18'), ('2016', '2016/17'), ('2015', '2015/16'), ('2014', '2014/15'), ('2013', '2013/14')])
|
||||
season = SelectField('Season data to search', choices=[('2020', '2020/21'), ('2019', '2019/20'), ('2018', '2018/19'), ('2017', '2017/18'), ('2016', '2016/17'), ('2015', '2015/16'), ('2014', '2014/15'), ('2013', '2013/14')])
|
||||
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')
|
||||
|
||||
|
||||
29
main.py
29
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,38 +20,39 @@ from routes import *
|
||||
app.register_blueprint(routes)
|
||||
|
||||
|
||||
@app.route('/hkfc-d/vote-chart', methods=['GET', 'POST'])
|
||||
def hkfc_d_vote_chart():
|
||||
form = LoginForm()
|
||||
user_lookup = "SELECT username FROM hockeyUsers WHERE (username= '" + form.username.data + "')"
|
||||
if form.validate_on_submit():
|
||||
rows = sql_read(user_lookup)
|
||||
return redirect(url_for('/hkfc-d/voting'))
|
||||
else:
|
||||
return render_template('hkfc-d/login-vote.html', form=form)
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
form = LoginForm()
|
||||
print('Here we are')
|
||||
user_lookup = "SELECT username FROM hockeyUsers WHERE (username= '" + form.username.data + "')"
|
||||
if form.validate_on_submit():
|
||||
sql = "SELECT username FROM hockeyUsers WHERE (username= '" + form.username.data + "')"
|
||||
print(sql)
|
||||
rows = sql_write(sql)
|
||||
print(rows)
|
||||
print(rows[0])
|
||||
return redirect(url_for('dashboard'))
|
||||
rows = sql_write(user_lookup)
|
||||
return redirect(url_for('/hkfc-d/voting'))
|
||||
else:
|
||||
return 'Something went wrong'
|
||||
# return '<h1>Something went wrong there</h1>'
|
||||
return render_template('login.html', form=form)
|
||||
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
form = RegisterForm()
|
||||
user_create = "INSERT INTO hockeyUsers (username, email, password) VALUES ('" + form.username.data + "', '" + form.email.data + "', '" + hashed_password + "')"
|
||||
if form.validate_on_submit():
|
||||
salt = uuid.uuid4().hex
|
||||
hashed_password = hashlib.sha512(form.password.data + salt).hexdigest()
|
||||
|
||||
sql = "INSERT INTO hockeyUsers (username, email, password) VALUES ('" + form.username.data + "', '" + form.email.data + "', '" + hashed_password + "')"
|
||||
print(sql)
|
||||
db = write_cloudsql()
|
||||
cursor = db.cursor()
|
||||
cursor.execute(sql)
|
||||
cursor.execute(user_create)
|
||||
db.commit()
|
||||
return '<h2>New user has been created!</h2>'
|
||||
|
||||
return render_template('register.html', form=form)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -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 *
|
||||
|
||||
@ -17,11 +17,9 @@ basic_auth = BasicAuth(app)
|
||||
|
||||
|
||||
@routes.route('/convenor/clubList')
|
||||
@basic_auth.required
|
||||
def convenorListClub():
|
||||
sql = "SELECT club, team, league from _clubTeams ORDER BY club, team"
|
||||
rows = sql_read(sql)
|
||||
print(rows)
|
||||
clubTeam_lookup = "SELECT club, team, league from _clubTeams ORDER BY club, team"
|
||||
rows = sql_read(clubTeam_lookup)
|
||||
table = clubList(rows)
|
||||
table.border = True
|
||||
table.classes = ['table-striped', 'table-condensed', 'table-hover']
|
||||
@ -29,92 +27,84 @@ def convenorListClub():
|
||||
|
||||
|
||||
@routes.route('/convenor/clubAdd')
|
||||
@basic_auth.required
|
||||
def convenorAddClub():
|
||||
form = addClubForm()
|
||||
return render_template('_convenorClubAdd.html', form = form)
|
||||
|
||||
@routes.route('/convenor/clubAddResult', methods=['POST'])
|
||||
@basic_auth.required
|
||||
def convenorAddClubResult():
|
||||
try:
|
||||
_club = request.form['clubName']
|
||||
club_lookup = "SELECT club FROM _clubTeams WHERE club='" + _club + "' GROUP BY club"
|
||||
club_create = "INSERT INTO _clubTeams (club, team) VALUES ('" + _club + "', 'A')"
|
||||
|
||||
# validate that this data has been entered
|
||||
if _club and request.method == 'POST':
|
||||
sql = "SELECT club FROM _clubTeams WHERE club='" + _club + "' GROUP BY club"
|
||||
clubExist = sql_read(sql)
|
||||
clubExist = sql_read(club_lookup)
|
||||
if clubExist:
|
||||
return 'Club already exists - try adding a team instead'
|
||||
else:
|
||||
sql2 = "INSERT INTO _clubTeams (club, team) VALUES ('" + _club + "', 'A')"
|
||||
sql_write(sql2)
|
||||
sql_write(club_create)
|
||||
return render_template('_convenorClubAddResults.html', data=_club)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
@routes.route('/convenor/teamAdd')
|
||||
@basic_auth.required
|
||||
def convenorAddTeam():
|
||||
sql = "SELECT club FROM _clubTeams GROUP BY club ORDER BY club"
|
||||
clubs = sql_read(sql)
|
||||
clubs_query = "SELECT club FROM _clubTeams GROUP BY club ORDER BY club"
|
||||
clubs = sql_read(clubs_query)
|
||||
form = addTeamForm()
|
||||
return render_template('_convenorTeamAdd.html', data=clubs, form=form)
|
||||
|
||||
@routes.route('/convenor/teamAddResult', methods=['POST'])
|
||||
@basic_auth.required
|
||||
def convenorAddTeamResult():
|
||||
try:
|
||||
_club = request.form['clubName']
|
||||
_team = request.form['teamName']
|
||||
clubTeam_lookup = "SELECT club, team FROM _clubTeams WHERE club='" + _club + "' AND team='" + _team + "'"
|
||||
clubTeam_create = "INSERT INTO _clubTeams (club, team) VALUES ('" + _club + "', '" + _team + "')"
|
||||
if _club and _team and request.method == 'POST':
|
||||
sql = "SELECT club, team FROM _clubTeams WHERE club='" + _club + "' AND team='" + _team + "'"
|
||||
teamExist = sql_read(sql)
|
||||
teamExist = sql_read(clubTeam_lookup)
|
||||
if teamExist:
|
||||
return 'Team already exists in the database'
|
||||
else:
|
||||
sql2 = "INSERT INTO _clubTeams (club, team) VALUES ('" + _club + "', '" + _team + "')"
|
||||
sql_write(sql2)
|
||||
sql_write(clubTeam_create)
|
||||
return render_template('_convenorTeamAddResults.html', club=_club, team=_team)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
@routes.route('/convenor/playerDbCreate')
|
||||
@basic_auth.required
|
||||
def playerDbCreate():
|
||||
sql = "SELECT club FROM _clubTeams GROUP BY club ORDER BY club"
|
||||
clubs = sql_read(sql)
|
||||
club_lookup = "SELECT club FROM _clubTeams GROUP BY club ORDER BY club"
|
||||
clubs = sql_read(club_lookup)
|
||||
form = playerDbCreateForm()
|
||||
return render_template('_convenorPlayerDbCreate.html', data=clubs, form=form)
|
||||
|
||||
@routes.route('/convenor/playerDbCreateResults', methods=['POST'])
|
||||
@basic_auth.required
|
||||
def playerDbCreateResults():
|
||||
try:
|
||||
_club = request.form['clubName']
|
||||
# _year = request.form['year']
|
||||
_year = "2018"
|
||||
playerTable_create = "CREATE TABLE IF NOT EXISTS _" + _club + "_players (playerTeam varchar(6) NOT NULL, playerForenames varchar(50) NOT NULL, playerSurname varchar(30) NOT NULL, playerNickName varchar(30), playerChineseName varchar(10) CHARACTER SET utf8, playerEmail varchar(255) NOT NULL, playerDob DATE NOT NULL, playerHkid varchar(20) NOT NULL, playerNumber smallint NOT NULL, playerTelNumber varchar(30) NOT NULL, PRIMARY KEY (playerNumber))"
|
||||
if _club and request.method == 'POST':
|
||||
sql = "CREATE TABLE IF NOT EXISTS _" + _club + "_players (playerTeam varchar(6) NOT NULL, playerForenames varchar(50) NOT NULL, playerSurname varchar(30) NOT NULL, playerNickName varchar(30), playerChineseName varchar(10) CHARACTER SET utf8, playerEmail varchar(255) NOT NULL, playerDob DATE NOT NULL, playerHkid varchar(20) NOT NULL, playerNumber smallint NOT NULL, playerTelNumber varchar(30) NOT NULL, PRIMARY KEY (playerNumber))"
|
||||
sql_write(sql)
|
||||
sql_write(playerTable_create)
|
||||
return render_template('_convenorPlayerDbCreateResults.html', club=_club, year=_year)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
@routes.route('/convenor/playerAdd')
|
||||
@basic_auth.required
|
||||
def convenorAddPlayer():
|
||||
sql = "SELECT hockeyClub, logoURL FROM mensHockeyClubs ORDER BY hockeyClub"
|
||||
clubs = sql_read_static(sql)
|
||||
clubLogo_lookup = "SELECT hockeyClub, logoURL FROM mensHockeyClubs ORDER BY hockeyClub"
|
||||
clubs = sql_read_static(clubLogo_lookup)
|
||||
form = addPlayerForm()
|
||||
form.playerClub.choices = [(club['hockeyClub'], club['hockeyClub']) for club in clubs]
|
||||
clubLogo = clubs[0]['logoURL']
|
||||
return render_template('_convenorPlayerAdd.html', form=form, clubLogo=clubLogo)
|
||||
|
||||
@routes.route('/convenor/playerAddResult', methods=['POST'])
|
||||
@basic_auth.required
|
||||
def convenorAddPlayerResult():
|
||||
try:
|
||||
# _year = request.form['year']
|
||||
@ -132,30 +122,28 @@ def convenorAddPlayerResult():
|
||||
_playerHkid = request.form['playerHkid']
|
||||
_playerNumber = request.form['playerNumber']
|
||||
_playerTelNumber = request.form['playerTelNumber']
|
||||
playerRecord_create = "INSERT INTO _" + _club + "_players (playerTeam, playerForenames, playerSurname, playerNickname, playerEmail, playerDob, playerHkid, playerNumber, playerTelNumber) VALUES ('" + _team + "', '" + _playerForename + "', '" + _playerSurname + "', '" + _playerNickname + "', '" + _playerEmail + "', '" + _playerDob + "', '" + _playerHkid + "', '" + _playerNumber + "', '" + _playerTelNumber + "')"
|
||||
if _team and _playerSurname and _playerHkid and _playerNumber and request.method == 'POST':
|
||||
sql = "INSERT INTO _" + _club + "_players (playerTeam, playerForenames, playerSurname, playerNickname, playerEmail, playerDob, playerHkid, playerNumber, playerTelNumber) VALUES ('" + _team + "', '" + _playerForename + "', '" + _playerSurname + "', '" + _playerNickname + "', '" + _playerEmail + "', '" + _playerDob + "', '" + _playerHkid + "', '" + _playerNumber + "', '" + _playerTelNumber + "')"
|
||||
sql_write(sql)
|
||||
sql_write(playerRecord_create)
|
||||
return render_template('_convenorPlayerAddResults.html', club=_club, firstname=_playerForename, nickname=_playerNickname, surname=_surname, shirt=_playerNumber)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
@routes.route('/convenor/squadList')
|
||||
@basic_auth.required
|
||||
def convenorSquadList():
|
||||
sql = "SELECT team FROM _clubTeams WHERE club='HKFC' ORDER BY team"
|
||||
teams = sql_read(sql)
|
||||
team_lookup = "SELECT team FROM _clubTeams WHERE club='HKFC' ORDER BY team"
|
||||
teams = sql_read(team_lookup)
|
||||
form = squadListForm()
|
||||
return render_template('_convenorSquadList.html', data=teams, form=form)
|
||||
|
||||
@routes.route('/convenor/squadListResults', methods=['POST'])
|
||||
@basic_auth.required
|
||||
def convenorSquadListResults():
|
||||
try:
|
||||
_team = request.form['teamName']
|
||||
teamPlayer_lookup = "SELECT playerForenames, playerSurname, playerNickname, playerChineseName, playerEmail, playerDob, playerHkid, playerNumber, playerTelNumber FROM _HKFC_players WHERE (playerTeam='" + _team + "') ORDER BY playerNumber"
|
||||
# validate that this data has been entered
|
||||
if _team and request.method == 'POST':
|
||||
sql = "SELECT playerForenames, playerSurname, playerNickname, playerChineseName, playerEmail, playerDob, playerHkid, playerNumber, playerTelNumber FROM _HKFC_players WHERE (playerTeam='" + _team + "') ORDER BY playerNumber"
|
||||
rows = sql_read(sql)
|
||||
table = convenorSquadListTable(rows)
|
||||
table.border = True
|
||||
@ -168,14 +156,12 @@ def convenorSquadListResults():
|
||||
|
||||
|
||||
@routes.route('/convenor/editPlayer', methods=['POST'])
|
||||
@basic_auth.required
|
||||
def convenorEditPlayer():
|
||||
_playerNumber = request.args['playerNumber']
|
||||
sql = "SELECT playerTeam, playerForenames, playerSurname, playerNickname, playerChineseName, playerEmail, playerDob, playerHkid, playerNumber, playerTelNumber FROM _HKFC_players WHERE playerNumber='" + _playerNumber + "'"
|
||||
sql2 = "SELECT hockeyClub, logoURL FROM mensHockeyClubs ORDER BY hockeyClub"
|
||||
playerData = sql_read(sql)
|
||||
print(playerData)
|
||||
clubs = sql_read_static(sql2)
|
||||
player_lookup = "SELECT playerTeam, playerForenames, playerSurname, playerNickname, playerChineseName, playerEmail, playerDob, playerHkid, playerNumber, playerTelNumber FROM _HKFC_players WHERE playerNumber='" + _playerNumber + "'"
|
||||
clubLogo_lookup = "SELECT hockeyClub, logoURL FROM mensHockeyClubs ORDER BY hockeyClub"
|
||||
playerData = sql_read(player_lookup)
|
||||
clubs = sql_read_static(clubLogo_lookup)
|
||||
form = addPlayerForm()
|
||||
form.playerClub.choices = [(club['hockeyClub'], club['hockeyClub']) for club in clubs]
|
||||
form.playerForenames.data = playerData[0]['playerForenames']
|
||||
@ -191,16 +177,14 @@ def convenorEditPlayer():
|
||||
|
||||
|
||||
@routes.route('/convenor/deletePlayer', methods=['POST'])
|
||||
@basic_auth.required
|
||||
def convenorDeletePlayer():
|
||||
_playerNumber = request.args['playerNumber']
|
||||
sql = "DELETE FROM _HKFC_players WHERE playerNumber=" + _playerNumber + ""
|
||||
sql_write(sql)
|
||||
player_delete = "DELETE FROM _HKFC_players WHERE playerNumber=" + _playerNumber + ""
|
||||
sql_write(player_delete)
|
||||
return render_template('_hkfcPlayerDeleted.html', number=_playerNumber)
|
||||
|
||||
|
||||
@routes.route('/convenor/editPlayerResult', methods=['POST'])
|
||||
@basic_auth.required
|
||||
def convenorEditPlayerResult():
|
||||
try:
|
||||
_club = request.form['playerClub']
|
||||
@ -216,19 +200,18 @@ def convenorEditPlayerResult():
|
||||
_playerHkid = request.form['playerHkid']
|
||||
_playerNumber = request.form['playerNumber']
|
||||
_playerTelNumber = request.form['playerTelNumber']
|
||||
player_update = "UPDATE _" + _club + "_players SET playerTeam='" + _team + "', playerForenames='" + _playerForename + "', playerSurname='" + _playerSurname + "', playerNickname='" + _playerNickname + "', playerEmail='" + _playerEmail + "', playerTelNumber='" + _playerTelNumber + "' WHERE playerHkid='" + _playerHkid + "'"
|
||||
if _team and _playerSurname and _playerHkid and _playerNumber and request.method == 'POST':
|
||||
sql = "UPDATE _" + _club + "_players SET playerTeam='" + _team + "', playerForenames='" + _playerForename + "', playerSurname='" + _playerSurname + "', playerNickname='" + _playerNickname + "', playerEmail='" + _playerEmail + "', playerTelNumber='" + _playerTelNumber + "' WHERE playerHkid='" + _playerHkid + "'"
|
||||
sql_write(sql)
|
||||
sql_write(player_update)
|
||||
return render_template('_convenorEditPlayerResults.html', club=_club, firstname=_playerForename, nickname=_playerNickname, surname=_playerSurname, shirt=_playerNumber)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
@routes.route('/convenor/fixtureList')
|
||||
@basic_auth.required
|
||||
def convenorFixturesList():
|
||||
sql = "SELECT date, division, homeTeam, awayTeam, venue, time, umpire1, umpire2 FROM hockeyFixtures"
|
||||
rows = sql_read(sql)
|
||||
fixtures_lookup = "SELECT date, division, homeTeam, awayTeam, venue, time, umpire1, umpire2 FROM hockeyFixtures"
|
||||
rows = sql_read(fixtures_lookup)
|
||||
table = convenorFixtureList(rows)
|
||||
table.border = True
|
||||
table.classes = ['table-striped', 'table-condensed', 'table-hover']
|
||||
|
||||
@ -3,14 +3,15 @@ import pymysql
|
||||
import os
|
||||
import json
|
||||
import datetime
|
||||
from datetime import datetime
|
||||
from flask import render_template, request, jsonify, flash
|
||||
from datetime import datetime, timedelta
|
||||
from flask import render_template, request, jsonify, flash, make_response
|
||||
from flask_basicauth import BasicAuth
|
||||
from app import app, randomUrlSuffix
|
||||
from readSettings import mySettings
|
||||
from dbWrite import sql_write, sql_write_static, sql_read, sql_read_static
|
||||
from tables import matchSquadTable, convenorFixtureList
|
||||
from forms import adminSettingsForm, motmForm, goalsAssistsForm, adminSettingsForm2
|
||||
from logging import warn, error
|
||||
from . import routes
|
||||
|
||||
app.config['BASIC_AUTH_USERNAME'] = 'admin'
|
||||
@ -19,10 +20,16 @@ basic_auth = BasicAuth(app)
|
||||
|
||||
@routes.route('/hkfc-d/motm/<randomUrlSuffix>')
|
||||
def hkfcD_motm_vote(randomUrlSuffix):
|
||||
sql = "SELECT playerNumber, playerForenames, playerSurname, playerNickname FROM _hkfcD_matchSquad ORDER BY RAND()"
|
||||
sql2 = "SELECT nextClub, nextTeam, nextDate, oppoLogo, hkfcLogo, currMotM, currDotD, nextFixture FROM hkfcDAdminSettings"
|
||||
rows = sql_read(sql)
|
||||
nextInfo = sql_read_static(sql2)
|
||||
squadPlayer_lookup = "SELECT playerNumber, playerForenames, playerSurname, playerNickname FROM _hkfcD_matchSquad ORDER BY RAND()"
|
||||
settings_lookup = "SELECT nextClub, nextTeam, nextDate, oppoLogo, hkfcLogo, currMotM, currDotD, nextFixture FROM hkfcDAdminSettings"
|
||||
nextFixture_lookup = "SELECT hockeyResults2020.hockeyFixtures.date, hockeyResults.hkfcDAdminSettings.nextFixture FROM hockeyResults2020.hockeyFixtures INNER JOIN hockeyResults.hkfcDAdminSettings ON hockeyResults2020.hockeyFixtures.fixtureNumber = hockeyResults.hkfcDAdminSettings.nextFixture"
|
||||
motmPicture_lookup = "SELECT playerPictureURL FROM _HKFC_players INNER JOIN hockeyResults.hkfcDAdminSettings ON _HKFC_players.playerNumber=hockeyResults.hkfcDAdminSettings.currMotM"
|
||||
dotdPicture_lookup = "SELECT playerPictureURL FROM _HKFC_players INNER JOIN hockeyResults.hkfcDAdminSettings ON _HKFC_players.playerNumber=hockeyResults.hkfcDAdminSettings.currDotD"
|
||||
comments_lookup = "SELECT comment FROM _motmComments INNER JOIN hockeyResults.hkfcDAdminSettings ON _motmComments.matchDate=hockeyResults.hkfcDAdminSettings.nextDate ORDER BY RAND() LIMIT 1"
|
||||
urlSuffix_lookup = "SELECT motmUrlSuffix FROM hockeyResults.hkfcDAdminSettings WHERE userid='admin'"
|
||||
|
||||
rows = sql_read(squadPlayer_lookup)
|
||||
nextInfo = sql_read_static(settings_lookup)
|
||||
nextClub = nextInfo[0]['nextClub']
|
||||
nextTeam = nextInfo[0]['nextTeam']
|
||||
nextFixture = nextInfo[0]['nextFixture']
|
||||
@ -31,27 +38,21 @@ def hkfcD_motm_vote(randomUrlSuffix):
|
||||
currMotM = nextInfo[0]['currMotM']
|
||||
currDotD = nextInfo[0]['currDotD']
|
||||
oppo = nextTeam
|
||||
sql3 = "SELECT hockeyResults2021.hockeyFixtures.date, hockeyResults.hkfcDAdminSettings.nextFixture FROM hockeyResults2021.hockeyFixtures INNER JOIN hockeyResults.hkfcDAdminSettings ON hockeyResults2021.hockeyFixtures.fixtureNumber = hockeyResults.hkfcDAdminSettings.nextFixture"
|
||||
nextMatchDate = sql_read(sql3)
|
||||
nextMatchDate = sql_read(nextFixture_lookup)
|
||||
nextDate = nextMatchDate[0]['date']
|
||||
formatDate = datetime.strftime(nextDate, '%A, %d %B %Y')
|
||||
|
||||
sql3 = "SELECT playerPictureURL FROM _HKFC_players INNER JOIN hockeyResults.hkfcDAdminSettings ON _HKFC_players.playerNumber=hockeyResults.hkfcDAdminSettings.currMotM"
|
||||
sql4 = "SELECT playerPictureURL FROM _HKFC_players INNER JOIN hockeyResults.hkfcDAdminSettings ON _HKFC_players.playerNumber=hockeyResults.hkfcDAdminSettings.currDotD"
|
||||
motm = sql_read(sql3)
|
||||
dotd = sql_read(sql4)
|
||||
motm = sql_read(motmPicture_lookup)
|
||||
dotd = sql_read(dotdPicture_lookup)
|
||||
motmURL = motm[0]['playerPictureURL']
|
||||
dotdURL = dotd[0]['playerPictureURL']
|
||||
|
||||
sql5 = "SELECT comment FROM _motmComments INNER JOIN hockeyResults.hkfcDAdminSettings ON _motmComments.matchDate=hockeyResults.hkfcDAdminSettings.nextDate ORDER BY RAND() LIMIT 1"
|
||||
comment = sql_read(sql5)
|
||||
comment = sql_read(comments_lookup)
|
||||
if comment == "":
|
||||
comment = "No comments added yet"
|
||||
form = motmForm()
|
||||
sql6 = "SELECT motmUrlSuffix FROM hockeyResults.hkfcDAdminSettings WHERE userid='admin'"
|
||||
urlSuff = sql_read_static(sql6)
|
||||
urlSuff = sql_read_static(urlSuffix_lookup)
|
||||
randomSuff = urlSuff[0]['motmUrlSuffix']
|
||||
print(randomSuff)
|
||||
if randomSuff == randomUrlSuffix:
|
||||
return render_template('_hkfcDMotmVote.html', data=rows, comment=comment, formatDate=formatDate, matchNumber=nextFixture, oppo=oppo, hkfcLogo=hkfcLogo, oppoLogo=oppoLogo, dotdURL=dotdURL, motmURL=motmURL, form=form)
|
||||
else:
|
||||
@ -59,8 +60,8 @@ def hkfcD_motm_vote(randomUrlSuffix):
|
||||
|
||||
@routes.route('/hkfc-d/comments', methods=['GET', 'POST'])
|
||||
def hkfcd_match_comments():
|
||||
sql = "SELECT nextClub, nextTeam, nextDate, oppoLogo, hkfcLogo FROM hkfcDAdminSettings"
|
||||
row = sql_read_static(sql)
|
||||
settings_lookup = "SELECT nextClub, nextTeam, nextDate, oppoLogo, hkfcLogo FROM hkfcDAdminSettings"
|
||||
row = sql_read_static(settings_lookup)
|
||||
# nextTeam already seems to include all the team+club details
|
||||
# _oppo = row[0]['nextClub'] + " " + row[0]['nextTeam']
|
||||
_oppo = row[0]['nextClub']
|
||||
@ -68,29 +69,31 @@ def hkfcd_match_comments():
|
||||
_matchDate = row[0]['nextDate'].strftime('%Y_%m_%d')
|
||||
hkfcLogo = row[0]['hkfcLogo']
|
||||
oppoLogo = row[0]['oppoLogo']
|
||||
comment_insert = "INSERT INTO _motmComments (matchDate, opposition, comment) VALUES ('" + commentDate + "', '" + _oppo + "', '" + _fixed_comment + "')"
|
||||
comment_lookup = "SELECT comment FROM _motmComments WHERE matchDate='" + _matchDate + "' ORDER BY RAND()"
|
||||
|
||||
if request.method == 'POST':
|
||||
_comment = request.form['matchComment']
|
||||
if _comment != 'Optional comments added here':
|
||||
_fixed_comment = _comment.replace("'", "\\'")
|
||||
sql3 = "INSERT INTO _motmComments (matchDate, opposition, comment) VALUES ('" + commentDate + "', '" + _oppo + "', '" + _fixed_comment + "')"
|
||||
sql_write(sql3)
|
||||
sql = "SELECT comment FROM _motmComments WHERE matchDate='" + _matchDate + "' ORDER BY RAND()"
|
||||
comments = sql_read(sql)
|
||||
sql_write(comment_insert)
|
||||
|
||||
comments = sql_read(comment_lookup)
|
||||
return render_template('_hkfcDMatchComments.html', comments=comments, hkfcLogo=hkfcLogo, oppoLogo=oppoLogo)
|
||||
|
||||
|
||||
@routes.route('/hkfc-d/statAdmin', methods=['GET', 'POST'])
|
||||
@basic_auth.required
|
||||
def hkfc_d_stats_admin():
|
||||
form = goalsAssistsForm()
|
||||
sql = "SELECT date, homeTeam, awayTeam, venue, fixtureNumber FROM hockeyFixtures WHERE homeTeam='HKFC D' OR awayTeam='HKFC D'"
|
||||
matches = sql_read(sql)
|
||||
fixtures_lookup = "SELECT date, homeTeam, awayTeam, venue, fixtureNumber FROM hockeyFixtures WHERE homeTeam='HKFC D' OR awayTeam='HKFC D'"
|
||||
squadPlayer_lookup = "SELECT playerNumber, playerNickname FROM _hkfcD_matchSquad"
|
||||
|
||||
matches = sql_read(fixtures_lookup)
|
||||
form.match.choices = [(match['fixtureNumber'], match['date']) for match in matches]
|
||||
sql2 = "SELECT playerNumber, playerNickname FROM _hkfcD_matchSquad"
|
||||
players = sql_read(sql2)
|
||||
players = sql_read(squadPlayer_lookup)
|
||||
return render_template('_goalsAssistsAdmin.html', data=players, form=form)
|
||||
|
||||
@routes.route('/hkfc-d/goalsAssistsSubmit', methods=['POST'])
|
||||
@basic_auth.required
|
||||
def goalsAssistsSubmit():
|
||||
try:
|
||||
data = request.form
|
||||
@ -99,9 +102,10 @@ def goalsAssistsSubmit():
|
||||
assists = request.form.getlist('assists')
|
||||
goals = request.form.getlist('goals')
|
||||
match = request.form['match']
|
||||
playerGoalsAssists_update = "INSERT INTO _hkfc_d_motm (playerNumber, playerName, assistsTotal, goalsTotal, assists_" + match + ", goals_" + match + ") SELECT playerNumber, playerNickname, '" + assists[idx] + "', '" + goals[idx] + "', '" + assists[idx] + "', '" + goals[idx] + "' FROM _HKFC_players WHERE playerNumber='" + player + "' ON DUPLICATE KEY UPDATE assistsTotal = assistsTotal + " + assists[idx] + ", goalsTotal = goalsTotal + " + goals[idx] + ", assists_" + match + " = " + assists[idx] + ", goals_" + match + " = " + goals[idx] + ""
|
||||
|
||||
for idx, player in enumerate(playerNumber):
|
||||
sql = "INSERT INTO _hkfc_d_motm (playerNumber, playerName, assistsTotal, goalsTotal, assists_" + match + ", goals_" + match + ") SELECT playerNumber, playerNickname, '" + assists[idx] + "', '" + goals[idx] + "', '" + assists[idx] + "', '" + goals[idx] + "' FROM _HKFC_players WHERE playerNumber='" + player + "' ON DUPLICATE KEY UPDATE assistsTotal = assistsTotal + " + assists[idx] + ", goalsTotal = goalsTotal + " + goals[idx] + ", assists_" + match + " = " + assists[idx] + ", goals_" + match + " = " + goals[idx] + ""
|
||||
sql_write(sql)
|
||||
sql_write(playerGoalsAssists_update)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
finally:
|
||||
@ -109,7 +113,6 @@ def goalsAssistsSubmit():
|
||||
|
||||
|
||||
@routes.route('/hkfc-d/motmAdmin', methods=['GET', 'POST'])
|
||||
@basic_auth.required
|
||||
def hkfcDMotmAdmin():
|
||||
form = adminSettingsForm2()
|
||||
prevFixture = mySettings('prevFixture')
|
||||
@ -136,19 +139,19 @@ def hkfcDMotmAdmin():
|
||||
if form.saveButton.data:
|
||||
flash('Settings saved!')
|
||||
urlSuffix = randomUrlSuffix(8)
|
||||
print(urlSuffix)
|
||||
sql3 = "UPDATE hkfcDAdminSettings SET motmUrlSuffix='" + urlSuffix + "' WHERE userid='admin'"
|
||||
sql_write_static(sql3)
|
||||
flash('MotM URL https://hockey.ervine.cloud/hkfc-d/motm/'+urlSuffix)
|
||||
flash('MotM URL https://hockey.ervine.dev/hkfc-d/motm/'+urlSuffix)
|
||||
elif form.activateButton.data:
|
||||
sql4 = "ALTER TABLE _hkfc_d_motm ADD COLUMN motm_" + _nextFixture + " smallint DEFAULT 0, ADD COLUMN dotd_" + _nextFixture + " smallint DEFAULT 0, ADD COLUMN assists_" + _nextFixture + " smallint DEFAULT 0, ADD COLUMN goals_" + _nextFixture + " smallint DEFAULT 0 "
|
||||
sql_write(sql4)
|
||||
sql5 = "SELECT motmUrlSuffix FROM hkfcDAdminSettings WHERE userid='admin'"
|
||||
tempSuffix = sql_read_static(sql5)
|
||||
sql5 = "ALTER TABLE motmSessions ADD COLUMN motm_" + _nextFixture + " smallint DEFAULT NULL, ADD COLUMN dotd_" + _nextFixture + " smallint DEFAULT NULL "
|
||||
sql_write(sql5)
|
||||
sql6 = "SELECT motmUrlSuffix FROM hkfcDAdminSettings WHERE userid='admin'"
|
||||
tempSuffix = sql_read_static(sql6)
|
||||
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.ervine.dev/hkfc-d/motm/'+currSuffix)
|
||||
else:
|
||||
flash('Something went wrong - check with Smithers')
|
||||
|
||||
@ -236,6 +239,51 @@ def hkfcD_vote_thanks():
|
||||
_oppo = request.form['oppo']
|
||||
|
||||
if _motm and _dotd and request.method == 'POST':
|
||||
prev_identity = request.cookies.get('sessionID')
|
||||
user_agent = request.headers.get('User-Agent')
|
||||
|
||||
if prev_identity:
|
||||
warn("Previous identity found: "+prev_identity)
|
||||
vote_query = "SELECT dotd_" + _matchDate + " FROM motmSessions WHERE sessionID='" + prev_identity + "'"
|
||||
vote_check = sql_read(vote_query)
|
||||
if not vote_check:
|
||||
warn('Cookie exists but no record in DB - check: '+prev_identity)
|
||||
return render_template('_hkfcDSmithersFail.html', sessionID=prev_identity)
|
||||
else:
|
||||
vote_valid = vote_check[0]['dotd_' + _matchDate ]
|
||||
if vote_valid:
|
||||
warn("Naughty, naughty, you've already voted!")
|
||||
return render_template('_hkfcDVoteFraud.html', sessionID=prev_identity)
|
||||
else:
|
||||
sql = "INSERT INTO _hkfc_d_motm (playerNumber, playerName, motmTotal, motm_" + _matchDate + ") SELECT playerNumber, playerNickname, '1', '1' FROM _HKFC_players WHERE playerNumber='" + _motm + "' ON DUPLICATE KEY UPDATE motmTotal = motmTotal + 1, motm_" + _matchDate + " = motm_" + _matchDate + " + 1"
|
||||
sql2 = "INSERT INTO _hkfc_d_motm (playerNumber, playerName, dotdTotal, dotd_" + _matchDate + ") SELECT playerNumber, playerNickname, '1', '1' FROM _HKFC_players WHERE playerNumber='" + _dotd + "' ON DUPLICATE KEY UPDATE dotdTotal = dotdTotal + 1, dotd_" + _matchDate + " = dotd_" + _matchDate + " + 1"
|
||||
sql4 = "UPDATE motmSessions SET motm_" + _matchDate + "=" + _motm + " WHERE sessionID='" + prev_identity + "'"
|
||||
sql5 = "UPDATE motmSessions SET dotd_" + _matchDate + "=" + _dotd + " WHERE sessionID='" + prev_identity + "'"
|
||||
sql_write(sql4)
|
||||
sql_write(sql5)
|
||||
if _comments == "":
|
||||
print("No comment")
|
||||
elif _comments == "Optional comments added here":
|
||||
print("No comment")
|
||||
else:
|
||||
### The matchDate has been replaced with the matchNumber - this should be corrected at some point (via a fixture table lookup)
|
||||
sql3 = "INSERT INTO _motmComments (_matchDate, opposition, comment) VALUES ('" + _matchDate + "', '" + _oppo + "', '" + _fixed_comments + "')"
|
||||
sql_write(sql3)
|
||||
sql_write(sql)
|
||||
sql_write(sql2)
|
||||
resp = make_response(render_template('_hkfcDVoteThanks.html'))
|
||||
expire_date = datetime.now()
|
||||
expire_date = expire_date + timedelta(days=90)
|
||||
|
||||
resp.set_cookie('sessionID', prev_identity, expires=expire_date)
|
||||
return resp
|
||||
else:
|
||||
identity = randomUrlSuffix(8)
|
||||
print("Identity: " + identity)
|
||||
print("User Agent: " + user_agent)
|
||||
warn("Identity and User-Agent set")
|
||||
id_commit = "INSERT INTO motmSessions (sessionID, userAgent, motm_" + _matchDate + ", dotd_" + _matchDate + ") VALUES ('" + identity + "', '" + user_agent + "', '" + _motm + "', '" + _dotd + "' )"
|
||||
sql_write(id_commit)
|
||||
sql = "INSERT INTO _hkfc_d_motm (playerNumber, playerName, motmTotal, motm_" + _matchDate + ") SELECT playerNumber, playerNickname, '1', '1' FROM _HKFC_players WHERE playerNumber='" + _motm + "' ON DUPLICATE KEY UPDATE motmTotal = motmTotal + 1, motm_" + _matchDate + " = motm_" + _matchDate + " + 1"
|
||||
sql2 = "INSERT INTO _hkfc_d_motm (playerNumber, playerName, dotdTotal, dotd_" + _matchDate + ") SELECT playerNumber, playerNickname, '1', '1' FROM _HKFC_players WHERE playerNumber='" + _dotd + "' ON DUPLICATE KEY UPDATE dotdTotal = dotdTotal + 1, dotd_" + _matchDate + " = dotd_" + _matchDate + " + 1"
|
||||
if _comments == "":
|
||||
@ -243,23 +291,96 @@ def hkfcD_vote_thanks():
|
||||
elif _comments == "Optional comments added here":
|
||||
print("No comment")
|
||||
else:
|
||||
### The matchDate has been replaced with the matchNumber - this should be corrected at some point (via a fixture table lookup)
|
||||
### The matchDate has been replaced with the matchNumber - this should be corrected at some point (via a fixture table lookup)
|
||||
sql3 = "INSERT INTO _motmComments (_matchDate, opposition, comment) VALUES ('" + _matchDate + "', '" + _oppo + "', '" + _fixed_comments + "')"
|
||||
sql_write(sql3)
|
||||
sql_write(sql)
|
||||
sql_write(sql2)
|
||||
return render_template('_hkfcDVoteThanks.html')
|
||||
#sql_write(sql3)
|
||||
#sql_write(sql)
|
||||
#sql_write(sql2)
|
||||
resp = make_response(render_template('_hkfcDVoteThanks.html'))
|
||||
expire_date = datetime.now()
|
||||
expire_date = expire_date + timedelta(days=90)
|
||||
|
||||
resp.set_cookie('sessionID', identity, expires=expire_date)
|
||||
return resp
|
||||
else:
|
||||
return 'Ouch ... something went wrong here'
|
||||
except Exception as e:
|
||||
print(e)
|
||||
finally:
|
||||
print('Votes cast')
|
||||
print('Votes cast - thanks')
|
||||
|
||||
@routes.route('/hkfc-d/vote-chicken', methods=['GET', 'POST'])
|
||||
def hkfcD_vote_chicken():
|
||||
try:
|
||||
prev_identity = request.cookies.get('sessionID')
|
||||
user_agent = request.headers.get('User-Agent')
|
||||
nextFixture = "SELECT nextFixture FROM hkfcDAdminSettings"
|
||||
_matchDate = sql_read_static(nextFixture)
|
||||
if prev_identity:
|
||||
vote_query = "SELECT dotd_" + _matchDate + " FROM motmSessions WHERE sessionID='" + prev_identity + "'"
|
||||
vote_check = sql_read(vote_query)
|
||||
if not vote_check:
|
||||
warn('Cookie exists but no record in DB - check: '+prev_identity)
|
||||
return render_template('_hkfcDSmithersFail.html', sessionID=prev_identity)
|
||||
else:
|
||||
vote_valid = vote_check[0]['dotd_' + _matchDate ]
|
||||
if vote_valid:
|
||||
warn("Naughty, naughty, you've already voted!")
|
||||
return render_template('_hkfcDVoteFraud.html', sessionID=prev_identity)
|
||||
else:
|
||||
sql = "INSERT INTO _hkfc_d_motm (playerNumber, playerName, motmTotal, motm_" + _matchDate + ") SELECT playerNumber, playerNickname, '1', '1' FROM _HKFC_players WHERE playerNumber='" + _motm + "' ON DUPLICATE KEY UPDATE motmTotal = motmTotal + 1, motm_" + _matchDate + " = motm_" + _matchDate + " + 1"
|
||||
sql2 = "INSERT INTO _hkfc_d_motm (playerNumber, playerName, dotdTotal, dotd_" + _matchDate + ") SELECT playerNumber, playerNickname, '1', '1' FROM _HKFC_players WHERE playerNumber='" + _dotd + "' ON DUPLICATE KEY UPDATE dotdTotal = dotdTotal + 1, dotd_" + _matchDate + " = dotd_" + _matchDate + " + 1"
|
||||
sql4 = "UPDATE motmSessions SET motm_" + _matchDate + "=" + _motm + " WHERE sessionID='" + prev_identity + "'"
|
||||
sql5 = "UPDATE motmSessions SET dotd_" + _matchDate + "=" + _dotd + " WHERE sessionID='" + prev_identity + "'"
|
||||
sql_write(sql4)
|
||||
sql_write(sql5)
|
||||
if _comments == "":
|
||||
print("No comment")
|
||||
elif _comments == "Optional comments added here":
|
||||
print("No comment")
|
||||
else:
|
||||
### The matchDate has been replaced with the matchNumber - this should be corrected at some point (via a fixture table lookup)
|
||||
sql3 = "INSERT INTO _motmComments (_matchDate, opposition, comment) VALUES ('" + _matchDate + "', '" + _oppo + "', '" + _fixed_comments + "')"
|
||||
sql_write(sql3)
|
||||
sql_write(sql)
|
||||
sql_write(sql2)
|
||||
resp = make_response(render_template('_hkfcDVoteThanks.html'))
|
||||
expire_date = datetime.now()
|
||||
expire_date = expire_date + timedelta(days=90)
|
||||
|
||||
resp.set_cookie('sessionID', prev_identity, expires=expire_date)
|
||||
return resp
|
||||
else:
|
||||
identity = randomUrlSuffix(8)
|
||||
id_commit = "INSERT INTO motmSessions (sessionID) VALUES ('" + identity + "')"
|
||||
ua_commit = "INSERT INTO motmSessions (userAgent) VALUES ('" + user_agent + "')"
|
||||
sql_write(id_commit)
|
||||
sql = "INSERT INTO _hkfc_d_motm (playerNumber, playerName, motmTotal, motm_" + _matchDate + ") SELECT playerNumber, playerNickname, '1', '1' FROM _HKFC_players WHERE playerNumber='" + _motm + "' ON DUPLICATE KEY UPDATE motmTotal = motmTotal + 1, motm_" + _matchDate + " = motm_" + _matchDate + " + 1"
|
||||
sql2 = "INSERT INTO _hkfc_d_motm (playerNumber, playerName, dotdTotal, dotd_" + _matchDate + ") SELECT playerNumber, playerNickname, '1', '1' FROM _HKFC_players WHERE playerNumber='" + _dotd + "' ON DUPLICATE KEY UPDATE dotdTotal = dotdTotal + 1, dotd_" + _matchDate + " = dotd_" + _matchDate + " + 1"
|
||||
if _comments == "":
|
||||
print("No comment")
|
||||
elif _comments == "Optional comments added here":
|
||||
print("No comment")
|
||||
else:
|
||||
### The matchDate has been replaced with the matchNumber - this should be corrected at some point (via a fixture table lookup)
|
||||
sql3 = "INSERT INTO _motmComments (_matchDate, opposition, comment) VALUES ('" + _matchDate + "', '" + _oppo + "', '" + _fixed_comments + "')"
|
||||
sql_write(sql3)
|
||||
sql_write(sql)
|
||||
sql_write(sql2)
|
||||
resp = make_response(render_template('_hkfcDVoteThanks.html'))
|
||||
expire_date = datetime.now()
|
||||
expire_date = expire_date + timedelta(days=90)
|
||||
resp.set_cookie('sessionID', identity, expires=expire_date)
|
||||
return resp
|
||||
#else:
|
||||
#return 'Ouch ... something went wrong here'
|
||||
except Exception as e:
|
||||
print(e)
|
||||
finally:
|
||||
print('Votes cast')
|
||||
@routes.route('/hkfc-d/vote-results')
|
||||
def hkfcD_vote_results():
|
||||
_matchDate = str(mySettings('fixture'))
|
||||
print(_matchDate)
|
||||
sql = "SELECT playerName, motm_" + _matchDate + ", dotd_" + _matchDate + " FROM _hkfc_d_motm WHERE (motm_" + _matchDate + " > '0') OR (dotd_" + _matchDate + " > '0')"
|
||||
print(sql)
|
||||
rows = sql_read(sql)
|
||||
@ -276,20 +397,17 @@ def hkfcD_poty_results():
|
||||
|
||||
|
||||
@routes.route('/hkfc-d/voting')
|
||||
@basic_auth.required
|
||||
def hkfcD_voting():
|
||||
matchDate = mySettings('fixture')
|
||||
return render_template('_hkfcDVoteChart.html', _matchDate=matchDate)
|
||||
|
||||
|
||||
@routes.route('/hkfc-d/poty')
|
||||
@basic_auth.required
|
||||
def hkfcD_poty():
|
||||
return render_template('_hkfcDPotYChart.html')
|
||||
|
||||
|
||||
@routes.route('/hkfc-d/matchSquad')
|
||||
@basic_auth.required
|
||||
def hkfcD_match_squad():
|
||||
sql1 = "SELECT team from _clubTeams WHERE club='HKFC' ORDER BY team"
|
||||
sql2 = "SELECT playerTeam, playerForenames, playerSurname, playerNickname, playerNumber FROM _HKFC_players"
|
||||
@ -298,7 +416,6 @@ def hkfcD_match_squad():
|
||||
return render_template('_hkfcDMatchSquad.html', teams=teams, players=players)
|
||||
|
||||
@routes.route('/hkfc-d/matchSquadSubmit', methods=['POST'])
|
||||
@basic_auth.required
|
||||
def hkfcD_match_squad_submit():
|
||||
_playerNumbers = request.form.getlist('playerNumber')
|
||||
for _playerNumber in _playerNumbers:
|
||||
@ -312,7 +429,6 @@ def hkfcD_match_squad_submit():
|
||||
return render_template('_hkfcDMatchSquadSelected.html', table=table)
|
||||
|
||||
@routes.route('/hkfc-d/matchSquadList')
|
||||
@basic_auth.required
|
||||
def hkfcD_match_squad_list():
|
||||
sql = "SELECT playerNumber, playerForenames, playerSurname, playerNickname FROM _hkfcD_matchSquad"
|
||||
players = sql_read(sql)
|
||||
@ -323,7 +439,6 @@ def hkfcD_match_squad_list():
|
||||
|
||||
|
||||
@routes.route('/convenor/delPlayerFromSquad', methods=['POST'])
|
||||
@basic_auth.required
|
||||
def delPlayerFromSquad():
|
||||
_playerNumber = request.args['playerNumber']
|
||||
sql = "DELETE FROM _hkfcD_matchSquad WHERE playerNumber=" + _playerNumber + ""
|
||||
@ -332,10 +447,8 @@ def delPlayerFromSquad():
|
||||
|
||||
|
||||
@routes.route('/hkfc-d/matchSquadReset')
|
||||
@basic_auth.required
|
||||
def hkfcD_matchSquadReset():
|
||||
_matchNumber = str(mySettings('fixture'))
|
||||
print(_matchNumber)
|
||||
sql1 = "RENAME TABLE _hkfcD_matchSquad TO _hkfcD_matchSquad_" + _matchNumber + ""
|
||||
sql2 = "CREATE TABLE _hkfcD_matchSquad (playerNumber smallint UNIQUE, playerForenames varchar(50), playerSurname varchar(30), playerNickname varchar(30) NOT NULL, PRIMARY KEY (playerNumber))"
|
||||
sql3 = "UPDATE hkfcDAdminSettings SET prevFixture='" + _matchNumber + "'"
|
||||
@ -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')
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<doctype html>
|
||||
<head>
|
||||
<title>Fixture List 2021/22 season</title>
|
||||
<title>Fixture List 2020/21 season</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">
|
||||
|
||||
@ -74,6 +74,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Submit</button>
|
||||
<a class="btn btn-warning" href="/hkfc-d/motm-chicken">Chicken</a>
|
||||
<a class="btn btn-danger" href="/dashboard" role="button">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user