Enhance match detail page with comprehensive functionality

Major Features Added:
- Navigation bar integration with consistent styling across all pages
- ZIP file management with upload/replace/delete functionality
- Real-time progress bars for ZIP uploads with visual feedback
- Complete outcomes management with editable values
- Add new outcomes functionality with validation
- Professional UI matching fixture detail page design

Backend Enhancements:
- New routes for outcome CRUD operations (/match/<id>/outcomes)
- AJAX-powered interactions for seamless user experience
- Proper error handling and user feedback systems
- Input validation for numeric outcome values
- User permission checks and security controls

Frontend Improvements:
- Responsive design optimized for mobile devices
- Real-time visual feedback for changed values
- Batch saving of multiple outcome changes
- Individual outcome deletion with confirmation
- Professional card-based layout for better information display
- Consistent boxing theme branding throughout

Technical Implementation:
- Database migration system with version tracking
- Fixture-based navigation and URL structure
- Event-driven JavaScript with proper event listeners
- Progress tracking with XMLHttpRequest for uploads
- Template inheritance with Jinja2 templating
- CSS grid layouts for responsive design

Files Modified:
- app/main/routes.py: Added outcome management routes
- app/templates/main/match_detail.html: Complete redesign
- app/database/: New migration system implementation
- Multiple template files: Navigation consistency updates

This commit provides a complete, professional match detail interface
with full editing capabilities and consistent user experience.
parent 8eb3aa0f
...@@ -31,6 +31,13 @@ def create_app(config_name=None): ...@@ -31,6 +31,13 @@ def create_app(config_name=None):
login_manager.login_message = 'Please log in to access this page.' login_manager.login_message = 'Please log in to access this page.'
login_manager.login_message_category = 'info' login_manager.login_message_category = 'info'
# Register user loader
@login_manager.user_loader
def load_user(user_id):
"""Load user for Flask-Login"""
from app.models import User
return User.query.get(int(user_id))
# Configure logging # Configure logging
setup_logging(app) setup_logging(app)
...@@ -47,9 +54,25 @@ def create_app(config_name=None): ...@@ -47,9 +54,25 @@ def create_app(config_name=None):
from app.upload import bp as upload_bp from app.upload import bp as upload_bp
app.register_blueprint(upload_bp, url_prefix='/upload') app.register_blueprint(upload_bp, url_prefix='/upload')
# Create database tables # Create database tables and run migrations (handle missing database gracefully)
with app.app_context(): with app.app_context():
db.create_all() try:
db.create_all()
# Run database migrations
from app.database import run_migrations
migration_result = run_migrations()
if migration_result['status'] == 'success':
app.logger.info(f"Database migrations completed: {migration_result['message']}")
elif migration_result['status'] == 'partial':
app.logger.warning(f"Database migrations partially completed: {migration_result['message']}")
elif migration_result['status'] == 'error':
app.logger.error(f"Database migrations failed: {migration_result['message']}")
except Exception as e:
app.logger.warning(f"Database initialization failed: {str(e)}")
app.logger.warning("Database may not exist or be accessible. Please check database configuration.")
return app return app
...@@ -57,10 +80,11 @@ def setup_logging(app): ...@@ -57,10 +80,11 @@ def setup_logging(app):
"""Setup application logging with colors for development""" """Setup application logging with colors for development"""
if not app.debug and not app.testing: if not app.debug and not app.testing:
# Production logging # Production logging
from logging import handlers as logging_handlers
if not os.path.exists('logs'): if not os.path.exists('logs'):
os.mkdir('logs') os.mkdir('logs')
file_handler = logging.handlers.RotatingFileHandler( file_handler = logging_handlers.RotatingFileHandler(
'logs/fixture-daemon.log', maxBytes=10240, backupCount=10 'logs/fixture-daemon.log', maxBytes=10240, backupCount=10
) )
file_handler.setFormatter(logging.Formatter( file_handler.setFormatter(logging.Formatter(
...@@ -87,9 +111,3 @@ def setup_logging(app): ...@@ -87,9 +111,3 @@ def setup_logging(app):
app.logger.addHandler(handler) app.logger.addHandler(handler)
app.logger.setLevel(logging.DEBUG) app.logger.setLevel(logging.DEBUG)
@login_manager.user_loader
def load_user(user_id):
"""Load user for Flask-Login"""
from app.models import User
return User.query.get(int(user_id))
\ No newline at end of file
...@@ -5,10 +5,8 @@ from flask_jwt_extended import jwt_required, get_jwt_identity ...@@ -5,10 +5,8 @@ from flask_jwt_extended import jwt_required, get_jwt_identity
from sqlalchemy import func, desc from sqlalchemy import func, desc
from app.api import bp from app.api import bp
from app import db from app import db
from app.models import Match, FileUpload, User, SystemLog, MatchOutcome, UserSession
from app.utils.security import require_admin, require_active_user from app.utils.security import require_admin, require_active_user
from app.utils.logging import log_api_request from app.utils.logging import log_api_request
from app.database import get_db_manager
from app.upload.file_handler import get_file_upload_handler from app.upload.file_handler import get_file_upload_handler
from app.upload.fixture_parser import get_fixture_parser from app.upload.fixture_parser import get_fixture_parser
...@@ -19,6 +17,7 @@ logger = logging.getLogger(__name__) ...@@ -19,6 +17,7 @@ logger = logging.getLogger(__name__)
def api_get_matches(): def api_get_matches():
"""Get matches with pagination and filtering""" """Get matches with pagination and filtering"""
try: try:
from app.models import User, Match, FileUpload
user_id = get_jwt_identity() user_id = get_jwt_identity()
user = User.query.get(user_id) user = User.query.get(user_id)
...@@ -121,6 +120,7 @@ def api_get_matches(): ...@@ -121,6 +120,7 @@ def api_get_matches():
def api_get_match(match_id): def api_get_match(match_id):
"""Get specific match details""" """Get specific match details"""
try: try:
from app.models import User, Match, FileUpload
user_id = get_jwt_identity() user_id = get_jwt_identity()
user = User.query.get(user_id) user = User.query.get(user_id)
...@@ -153,6 +153,7 @@ def api_get_match(match_id): ...@@ -153,6 +153,7 @@ def api_get_match(match_id):
def api_update_match(match_id): def api_update_match(match_id):
"""Update match details""" """Update match details"""
try: try:
from app.models import User, Match
user_id = get_jwt_identity() user_id = get_jwt_identity()
user = User.query.get(user_id) user = User.query.get(user_id)
...@@ -210,6 +211,7 @@ def api_update_match(match_id): ...@@ -210,6 +211,7 @@ def api_update_match(match_id):
def api_delete_match(match_id): def api_delete_match(match_id):
"""Delete match (admin only)""" """Delete match (admin only)"""
try: try:
from app.models import User, Match, FileUpload
user_id = get_jwt_identity() user_id = get_jwt_identity()
user = User.query.get(user_id) user = User.query.get(user_id)
...@@ -246,6 +248,7 @@ def api_delete_match(match_id): ...@@ -246,6 +248,7 @@ def api_delete_match(match_id):
def api_get_statistics(): def api_get_statistics():
"""Get comprehensive statistics""" """Get comprehensive statistics"""
try: try:
from app.models import User, Match, FileUpload
user_id = get_jwt_identity() user_id = get_jwt_identity()
user = User.query.get(user_id) user = User.query.get(user_id)
...@@ -273,6 +276,7 @@ def api_get_statistics(): ...@@ -273,6 +276,7 @@ def api_get_statistics():
# Global statistics (if admin) # Global statistics (if admin)
global_stats = {} global_stats = {}
if user.is_admin: if user.is_admin:
from app.database import get_db_manager
db_manager = get_db_manager() db_manager = get_db_manager()
global_stats = db_manager.get_database_stats() global_stats = db_manager.get_database_stats()
...@@ -319,6 +323,7 @@ def api_admin_get_users(): ...@@ -319,6 +323,7 @@ def api_admin_get_users():
search_query = request.args.get('search', '').strip() search_query = request.args.get('search', '').strip()
status_filter = request.args.get('status') status_filter = request.args.get('status')
from app.models import User
# Base query # Base query
query = User.query query = User.query
...@@ -367,6 +372,7 @@ def api_admin_get_users(): ...@@ -367,6 +372,7 @@ def api_admin_get_users():
def api_admin_update_user(user_id): def api_admin_update_user(user_id):
"""Update user (admin only)""" """Update user (admin only)"""
try: try:
from app.models import User
user = User.query.get(user_id) user = User.query.get(user_id)
if not user: if not user:
return jsonify({'error': 'User not found'}), 404 return jsonify({'error': 'User not found'}), 404
...@@ -413,6 +419,7 @@ def api_admin_get_logs(): ...@@ -413,6 +419,7 @@ def api_admin_get_logs():
level_filter = request.args.get('level') level_filter = request.args.get('level')
module_filter = request.args.get('module') module_filter = request.args.get('module')
from app.models import SystemLog
# Base query # Base query
query = SystemLog.query query = SystemLog.query
...@@ -450,6 +457,7 @@ def api_admin_get_logs(): ...@@ -450,6 +457,7 @@ def api_admin_get_logs():
def api_admin_system_info(): def api_admin_system_info():
"""Get system information (admin only)""" """Get system information (admin only)"""
try: try:
from app.database import get_db_manager
db_manager = get_db_manager() db_manager = get_db_manager()
# Database statistics # Database statistics
...@@ -466,6 +474,7 @@ def api_admin_system_info(): ...@@ -466,6 +474,7 @@ def api_admin_system_info():
fixture_parser = get_fixture_parser() fixture_parser = get_fixture_parser()
parsing_stats = fixture_parser.get_parsing_statistics() parsing_stats = fixture_parser.get_parsing_statistics()
from app.models import UserSession
# Active sessions # Active sessions
active_sessions = UserSession.query.filter_by(is_active=True).count() active_sessions = UserSession.query.filter_by(is_active=True).count()
...@@ -500,6 +509,7 @@ def api_admin_cleanup(): ...@@ -500,6 +509,7 @@ def api_admin_cleanup():
if cleanup_type in ['all', 'sessions']: if cleanup_type in ['all', 'sessions']:
# Clean up expired sessions # Clean up expired sessions
from app.database import get_db_manager
db_manager = get_db_manager() db_manager = get_db_manager()
expired_sessions = db_manager.cleanup_expired_sessions() expired_sessions = db_manager.cleanup_expired_sessions()
results['expired_sessions_cleaned'] = expired_sessions results['expired_sessions_cleaned'] = expired_sessions
...@@ -507,6 +517,7 @@ def api_admin_cleanup(): ...@@ -507,6 +517,7 @@ def api_admin_cleanup():
if cleanup_type in ['all', 'logs']: if cleanup_type in ['all', 'logs']:
# Clean up old logs (older than 30 days) # Clean up old logs (older than 30 days)
days = request.json.get('log_retention_days', 30) if request.json else 30 days = request.json.get('log_retention_days', 30) if request.json else 30
from app.database import get_db_manager
db_manager = get_db_manager() db_manager = get_db_manager()
old_logs = db_manager.cleanup_old_logs(days) old_logs = db_manager.cleanup_old_logs(days)
results['old_logs_cleaned'] = old_logs results['old_logs_cleaned'] = old_logs
......
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
from app.models import User
import re import re
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
...@@ -44,12 +43,14 @@ class RegistrationForm(FlaskForm): ...@@ -44,12 +43,14 @@ class RegistrationForm(FlaskForm):
raise ValidationError('Username can only contain letters, numbers, and underscores') raise ValidationError('Username can only contain letters, numbers, and underscores')
# Check if username already exists # Check if username already exists
from app.models import User
user = User.query.filter_by(username=username.data).first() user = User.query.filter_by(username=username.data).first()
if user is not None: if user is not None:
raise ValidationError('Username already exists. Please choose a different one.') raise ValidationError('Username already exists. Please choose a different one.')
def validate_email(self, email): def validate_email(self, email):
"""Validate email uniqueness""" """Validate email uniqueness"""
from app.models import User
user = User.query.filter_by(email=email.data).first() user = User.query.filter_by(email=email.data).first()
if user is not None: if user is not None:
raise ValidationError('Email already registered. Please use a different email address.') raise ValidationError('Email already registered. Please use a different email address.')
...@@ -143,6 +144,7 @@ class ForgotPasswordForm(FlaskForm): ...@@ -143,6 +144,7 @@ class ForgotPasswordForm(FlaskForm):
def validate_email(self, email): def validate_email(self, email):
"""Validate email exists""" """Validate email exists"""
from app.models import User
user = User.query.filter_by(email=email.data).first() user = User.query.filter_by(email=email.data).first()
if user is None: if user is None:
raise ValidationError('No account found with that email address.') raise ValidationError('No account found with that email address.')
...@@ -163,6 +165,139 @@ class ResetPasswordForm(FlaskForm): ...@@ -163,6 +165,139 @@ class ResetPasswordForm(FlaskForm):
"""Validate password strength""" """Validate password strength"""
password_value = password.data password_value = password.data
# Check minimum length
if len(password_value) < 8:
raise ValidationError('Password must be at least 8 characters long')
# Check for at least one uppercase letter
if not re.search(r'[A-Z]', password_value):
raise ValidationError('Password must contain at least one uppercase letter')
# Check for at least one lowercase letter
if not re.search(r'[a-z]', password_value):
raise ValidationError('Password must contain at least one lowercase letter')
# Check for at least one digit
if not re.search(r'\d', password_value):
raise ValidationError('Password must contain at least one number')
# Check for at least one special character
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password_value):
raise ValidationError('Password must contain at least one special character')
# Check for common weak passwords
weak_passwords = [
'password', '12345678', 'qwerty123', 'admin123',
'password123', '123456789', 'welcome123'
]
if password_value.lower() in weak_passwords:
raise ValidationError('Password is too common. Please choose a stronger password.')
class AdminUserForm(FlaskForm):
"""Admin user management form"""
username = StringField('Username', validators=[
DataRequired(message='Username is required'),
Length(min=3, max=80, message='Username must be between 3 and 80 characters')
])
email = StringField('Email', validators=[
DataRequired(message='Email is required'),
Email(message='Invalid email address'),
Length(max=120, message='Email must be less than 120 characters')
])
is_active = BooleanField('Active')
is_admin = BooleanField('Administrator')
submit = SubmitField('Update User')
class AdminCreateUserForm(FlaskForm):
"""Admin create user form"""
username = StringField('Username', validators=[
DataRequired(message='Username is required'),
Length(min=3, max=80, message='Username must be between 3 and 80 characters')
])
email = StringField('Email', validators=[
DataRequired(message='Email is required'),
Email(message='Invalid email address'),
Length(max=120, message='Email must be less than 120 characters')
])
password = PasswordField('Password', validators=[
DataRequired(message='Password is required'),
Length(min=8, message='Password must be at least 8 characters long')
])
password2 = PasswordField('Repeat Password', validators=[
DataRequired(message='Please confirm your password'),
EqualTo('password', message='Passwords must match')
])
is_active = BooleanField('Active', default=True)
is_admin = BooleanField('Administrator')
submit = SubmitField('Create User')
def validate_username(self, username):
"""Validate username uniqueness and format"""
# Check for valid characters (alphanumeric and underscore only)
if not re.match(r'^[a-zA-Z0-9_]+$', username.data):
raise ValidationError('Username can only contain letters, numbers, and underscores')
# Check if username already exists
from app.models import User
user = User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError('Username already exists. Please choose a different one.')
def validate_email(self, email):
"""Validate email uniqueness"""
from app.models import User
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('Email already registered. Please use a different email address.')
def validate_password(self, password):
"""Validate password strength"""
password_value = password.data
# Check minimum length
if len(password_value) < 8:
raise ValidationError('Password must be at least 8 characters long')
# Check for at least one uppercase letter
if not re.search(r'[A-Z]', password_value):
raise ValidationError('Password must contain at least one uppercase letter')
# Check for at least one lowercase letter
if not re.search(r'[a-z]', password_value):
raise ValidationError('Password must contain at least one lowercase letter')
# Check for at least one digit
if not re.search(r'\d', password_value):
raise ValidationError('Password must contain at least one number')
# Check for at least one special character
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password_value):
raise ValidationError('Password must contain at least one special character')
# Check for common weak passwords
weak_passwords = [
'password', '12345678', 'qwerty123', 'admin123',
'password123', '123456789', 'welcome123'
]
if password_value.lower() in weak_passwords:
raise ValidationError('Password is too common. Please choose a stronger password.')
class AdminResetPasswordForm(FlaskForm):
"""Admin reset user password form"""
new_password = PasswordField('New Password', validators=[
DataRequired(message='New password is required'),
Length(min=8, message='Password must be at least 8 characters long')
])
new_password2 = PasswordField('Repeat New Password', validators=[
DataRequired(message='Please confirm the new password'),
EqualTo('new_password', message='Passwords must match')
])
submit = SubmitField('Reset Password')
def validate_new_password(self, new_password):
"""Validate new password strength"""
password_value = new_password.data
# Check minimum length # Check minimum length
if len(password_value) < 8: if len(password_value) < 8:
raise ValidationError('Password must be at least 8 characters long') raise ValidationError('Password must be at least 8 characters long')
......
...@@ -6,7 +6,6 @@ from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identi ...@@ -6,7 +6,6 @@ from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identi
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
from app.auth import bp from app.auth import bp
from app import db from app import db
from app.models import User, UserSession, SystemLog
from app.auth.forms import LoginForm, RegistrationForm from app.auth.forms import LoginForm, RegistrationForm
from app.utils.security import validate_password_strength, generate_secure_token, rate_limit_check from app.utils.security import validate_password_strength, generate_secure_token, rate_limit_check
from app.utils.logging import log_security_event from app.utils.logging import log_security_event
...@@ -29,6 +28,7 @@ def login(): ...@@ -29,6 +28,7 @@ def login():
flash('Too many login attempts. Please try again later.', 'error') flash('Too many login attempts. Please try again later.', 'error')
return render_template('auth/login.html', form=form) return render_template('auth/login.html', form=form)
from app.models import User, UserSession
user = User.query.filter_by(username=form.username.data).first() user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(form.password.data): if user and user.check_password(form.password.data):
...@@ -75,6 +75,7 @@ def logout(): ...@@ -75,6 +75,7 @@ def logout():
# Deactivate user session # Deactivate user session
if 'session_id' in session: if 'session_id' in session:
from app.models import UserSession
user_session = UserSession.query.filter_by(session_id=session['session_id']).first() user_session = UserSession.query.filter_by(session_id=session['session_id']).first()
if user_session: if user_session:
user_session.deactivate() user_session.deactivate()
...@@ -108,6 +109,7 @@ def register(): ...@@ -108,6 +109,7 @@ def register():
return render_template('auth/register.html', form=form) return render_template('auth/register.html', form=form)
# Check if user already exists # Check if user already exists
from app.models import User
if User.query.filter_by(username=form.username.data).first(): if User.query.filter_by(username=form.username.data).first():
flash('Username already exists.', 'error') flash('Username already exists.', 'error')
return render_template('auth/register.html', form=form) return render_template('auth/register.html', form=form)
...@@ -153,6 +155,7 @@ def api_login(): ...@@ -153,6 +155,7 @@ def api_login():
log_security_event('API_LOGIN_RATE_LIMIT', client_ip, username=data.get('username')) log_security_event('API_LOGIN_RATE_LIMIT', client_ip, username=data.get('username'))
return jsonify({'error': 'Too many login attempts'}), 429 return jsonify({'error': 'Too many login attempts'}), 429
from app.models import User, UserSession
user = User.query.filter_by(username=data['username']).first() user = User.query.filter_by(username=data['username']).first()
if user and user.check_password(data['password']): if user and user.check_password(data['password']):
...@@ -206,6 +209,7 @@ def api_logout(): ...@@ -206,6 +209,7 @@ def api_logout():
session_id = data.get('session_id') session_id = data.get('session_id')
if session_id: if session_id:
from app.models import UserSession
user_session = UserSession.query.filter_by( user_session = UserSession.query.filter_by(
session_id=session_id, session_id=session_id,
user_id=user_id user_id=user_id
...@@ -227,6 +231,7 @@ def api_refresh(): ...@@ -227,6 +231,7 @@ def api_refresh():
"""Refresh JWT token""" """Refresh JWT token"""
try: try:
user_id = get_jwt_identity() user_id = get_jwt_identity()
from app.models import User
user = User.query.get(user_id) user = User.query.get(user_id)
if not user or not user.is_active: if not user or not user.is_active:
...@@ -253,6 +258,7 @@ def api_profile(): ...@@ -253,6 +258,7 @@ def api_profile():
"""Get user profile""" """Get user profile"""
try: try:
user_id = get_jwt_identity() user_id = get_jwt_identity()
from app.models import User
user = User.query.get(user_id) user = User.query.get(user_id)
if not user: if not user:
...@@ -270,6 +276,7 @@ def api_change_password(): ...@@ -270,6 +276,7 @@ def api_change_password():
"""Change user password""" """Change user password"""
try: try:
user_id = get_jwt_identity() user_id = get_jwt_identity()
from app.models import User
user = User.query.get(user_id) user = User.query.get(user_id)
if not user: if not user:
...@@ -309,6 +316,7 @@ def api_user_sessions(): ...@@ -309,6 +316,7 @@ def api_user_sessions():
"""Get user's active sessions""" """Get user's active sessions"""
try: try:
user_id = get_jwt_identity() user_id = get_jwt_identity()
from app.models import UserSession
sessions = UserSession.query.filter_by( sessions = UserSession.query.filter_by(
user_id=user_id, user_id=user_id,
is_active=True is_active=True
...@@ -328,6 +336,7 @@ def api_terminate_session(session_id): ...@@ -328,6 +336,7 @@ def api_terminate_session(session_id):
"""Terminate a specific session""" """Terminate a specific session"""
try: try:
user_id = get_jwt_identity() user_id = get_jwt_identity()
from app.models import UserSession
user_session = UserSession.query.filter_by( user_session = UserSession.query.filter_by(
session_id=session_id, session_id=session_id,
user_id=user_id user_id=user_id
......
"""
Database package for Fixture Manager
Handles migrations and database versioning
"""
from .migrations import (
get_migration_manager,
run_migrations,
get_migration_status,
DatabaseVersion
)
# Import functions from the manager module for backward compatibility
from .manager import (
init_database_manager,
get_db_manager,
DatabaseManager
)
__all__ = [
'get_migration_manager',
'run_migrations',
'get_migration_status',
'DatabaseVersion',
'init_database_manager',
'get_db_manager',
'DatabaseManager'
]
\ No newline at end of file
...@@ -6,7 +6,6 @@ from sqlalchemy.exc import SQLAlchemyError, OperationalError ...@@ -6,7 +6,6 @@ from sqlalchemy.exc import SQLAlchemyError, OperationalError
from sqlalchemy.pool import QueuePool from sqlalchemy.pool import QueuePool
from flask import current_app from flask import current_app
from app import db from app import db
from app.models import User, Match, MatchOutcome, FileUpload, SystemLog, UserSession
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -151,6 +150,7 @@ class DatabaseManager: ...@@ -151,6 +150,7 @@ class DatabaseManager:
"""Create default admin user if it doesn't exist""" """Create default admin user if it doesn't exist"""
try: try:
with self.app.app_context(): with self.app.app_context():
from app.models import User
admin_user = User.query.filter_by(username='admin').first() admin_user = User.query.filter_by(username='admin').first()
if not admin_user: if not admin_user:
admin_user = User( admin_user = User(
...@@ -173,6 +173,7 @@ class DatabaseManager: ...@@ -173,6 +173,7 @@ class DatabaseManager:
"""Get database statistics""" """Get database statistics"""
try: try:
with self.app.app_context(): with self.app.app_context():
from app.models import User, Match, MatchOutcome, FileUpload, SystemLog, UserSession
stats = { stats = {
'users': User.query.count(), 'users': User.query.count(),
'matches': Match.query.count(), 'matches': Match.query.count(),
...@@ -192,6 +193,7 @@ class DatabaseManager: ...@@ -192,6 +193,7 @@ class DatabaseManager:
try: try:
with self.app.app_context(): with self.app.app_context():
from datetime import datetime from datetime import datetime
from app.models import UserSession
expired_sessions = UserSession.query.filter( expired_sessions = UserSession.query.filter(
UserSession.expires_at < datetime.utcnow() UserSession.expires_at < datetime.utcnow()
).all() ).all()
...@@ -213,6 +215,7 @@ class DatabaseManager: ...@@ -213,6 +215,7 @@ class DatabaseManager:
try: try:
with self.app.app_context(): with self.app.app_context():
from datetime import datetime, timedelta from datetime import datetime, timedelta
from app.models import SystemLog
cutoff_date = datetime.utcnow() - timedelta(days=days) cutoff_date = datetime.utcnow() - timedelta(days=days)
old_logs = SystemLog.query.filter( old_logs = SystemLog.query.filter(
......
This diff is collapsed.
This diff is collapsed.
...@@ -71,7 +71,7 @@ class Match(db.Model): ...@@ -71,7 +71,7 @@ class Match(db.Model):
result = db.Column(db.String(255)) result = db.Column(db.String(255))
filename = db.Column(db.String(1024), nullable=False) filename = db.Column(db.String(1024), nullable=False)
file_sha1sum = db.Column(db.String(255), nullable=False, index=True) file_sha1sum = db.Column(db.String(255), nullable=False, index=True)
fixture_id = db.Column(db.String(255), unique=True, nullable=False, index=True) fixture_id = db.Column(db.String(255), nullable=False, index=True)
active_status = db.Column(db.Boolean, default=False, index=True) active_status = db.Column(db.Boolean, default=False, index=True)
# ZIP file related fields # ZIP file related fields
...@@ -93,6 +93,7 @@ class Match(db.Model): ...@@ -93,6 +93,7 @@ class Match(db.Model):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(Match, self).__init__(**kwargs) super(Match, self).__init__(**kwargs)
# Only generate fixture_id if not provided
if not self.fixture_id: if not self.fixture_id:
self.fixture_id = str(uuid.uuid4()) self.fixture_id = str(uuid.uuid4())
...@@ -154,6 +155,22 @@ class Match(db.Model): ...@@ -154,6 +155,22 @@ class Match(db.Model):
return data return data
@classmethod
def get_matches_by_fixture(cls, fixture_id: str):
"""Get all matches for a specific fixture"""
return cls.query.filter_by(fixture_id=fixture_id).all()
@classmethod
def get_fixtures_summary(cls):
"""Get summary of all fixtures with match counts"""
from sqlalchemy import func
return db.session.query(
cls.fixture_id,
cls.filename,
func.count(cls.id).label('match_count'),
func.min(cls.created_at).label('created_at')
).group_by(cls.fixture_id, cls.filename).all()
def __repr__(self): def __repr__(self):
return f'<Match {self.match_number}: {self.fighter1_township} vs {self.fighter2_township}>' return f'<Match {self.match_number}: {self.fighter1_township} vs {self.fighter2_township}>'
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Fixture Manager</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 400px;
width: 90%;
}
.logo {
text-align: center;
font-size: 2rem;
color: #333;
margin-bottom: 1rem;
font-weight: bold;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #333;
font-weight: bold;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
font-size: 1rem;
}
input[type="text"]:focus, input[type="password"]:focus {
outline: none;
border-color: #007bff;
}
.btn {
width: 100%;
padding: 12px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #0056b3;
}
.links {
text-align: center;
margin-top: 1rem;
}
.links a {
color: #007bff;
text-decoration: none;
}
.links a:hover {
text-decoration: underline;
}
.alert {
padding: 12px;
margin-bottom: 1rem;
border-radius: 4px;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
</style>
</head>
<body>
<div class="container">
<div class="logo">🥊 Fixture Manager</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.username.label }}
{{ form.username(class="form-control") }}
</div>
<div class="form-group">
{{ form.password.label }}
{{ form.password(class="form-control") }}
</div>
<div class="form-group">
{{ form.submit(class="btn") }}
</div>
</form>
<div class="links">
<a href="{{ url_for('auth.register') }}">Don't have an account? Register here</a><br>
<a href="{{ url_for('main.index') }}">← Back to Home</a>
</div>
</div>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - Fixture Manager</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 400px;
width: 90%;
}
.logo {
text-align: center;
font-size: 2rem;
color: #333;
margin-bottom: 1rem;
font-weight: bold;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #333;
font-weight: bold;
}
input[type="text"], input[type="email"], input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
font-size: 1rem;
}
input[type="text"]:focus, input[type="email"]:focus, input[type="password"]:focus {
outline: none;
border-color: #007bff;
}
.btn {
width: 100%;
padding: 12px;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #218838;
}
.links {
text-align: center;
margin-top: 1rem;
}
.links a {
color: #007bff;
text-decoration: none;
}
.links a:hover {
text-decoration: underline;
}
.alert {
padding: 12px;
margin-bottom: 1rem;
border-radius: 4px;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.password-requirements {
font-size: 0.9rem;
color: #666;
margin-top: 0.5rem;
}
.password-requirements ul {
margin: 0.5rem 0;
padding-left: 1.5rem;
}
</style>
</head>
<body>
<div class="container">
<div class="logo">🥊 Fixture Manager</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.username.label }}
{{ form.username(class="form-control") }}
</div>
<div class="form-group">
{{ form.email.label }}
{{ form.email(class="form-control") }}
</div>
<div class="form-group">
{{ form.password.label }}
{{ form.password(class="form-control") }}
<div class="password-requirements">
Password requirements:
<ul>
<li>At least 8 characters long</li>
<li>Contains uppercase and lowercase letters</li>
<li>Contains at least one number</li>
<li>Contains at least one special character</li>
</ul>
</div>
</div>
<div class="form-group">
{{ form.password2.label }}
{{ form.password2(class="form-control") }}
</div>
<div class="form-group">
{{ form.submit(class="btn") }}
</div>
</form>
<div class="links">
<a href="{{ url_for('auth.login') }}">Already have an account? Login here</a><br>
<a href="{{ url_for('main.index') }}">← Back to Home</a>
</div>
</div>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel - Fixture Manager</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.header {
background-color: #007bff;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
}
.nav {
display: flex;
gap: 1rem;
}
.nav a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.3s;
}
.nav a:hover {
background-color: rgba(255,255,255,0.1);
}
.container { max-width: 1200px; margin: 2rem auto; padding: 0 2rem; background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.btn { padding: 8px 16px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; display: inline-block; margin: 0.5rem 0; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin: 1rem 0; }
.stat-card { background: #f8f9fa; padding: 1.5rem; border-radius: 8px; text-align: center; }
.stat-number { font-size: 2rem; font-weight: bold; color: #dc3545; }
.stat-label { color: #666; font-size: 0.9rem; }
.admin-links { display: flex; gap: 1rem; flex-wrap: wrap; margin: 2rem 0; }
</style>
</head>
<body>
<div class="header">
<div class="logo">🥊 Fixture Manager</div>
<div class="nav">
<a href="{{ url_for('main.dashboard') }}">Dashboard</a>
<a href="{{ url_for('main.fixtures') }}">Fixtures</a>
<a href="{{ url_for('main.uploads') }}">Uploads</a>
<a href="{{ url_for('main.statistics') }}">Statistics</a>
{% if current_user.is_admin %}
<a href="{{ url_for('main.admin_panel') }}">Admin</a>
{% endif %}
<a href="{{ url_for('auth.logout') }}">Logout</a>
</div>
</div>
<div class="container">
<h1>Admin Panel</h1>
<div class="admin-links">
<a href="{{ url_for('main.admin_users') }}" class="btn">Manage Users</a>
<a href="{{ url_for('main.admin_logs') }}" class="btn">View System Logs</a>
</div>
{% if system_stats %}
<h2>System Overview</h2>
<div class="stats-grid">
{% for key, value in system_stats.items() %}
<div class="stat-card">
<div class="stat-number">{{ value }}</div>
<div class="stat-label">{{ key.replace('_', ' ').title() }}</div>
</div>
{% endfor %}
</div>
{% endif %}
{% if user_stats %}
<h2>User Statistics</h2>
<div class="stats-grid">
{% for key, value in user_stats.items() %}
<div class="stat-card">
<div class="stat-number">{{ value }}</div>
<div class="stat-label">{{ key.replace('_', ' ').title() }}</div>
</div>
{% endfor %}
</div>
{% endif %}
{% if recent_logs %}
<h2>Recent System Logs</h2>
<div style="max-height: 400px; overflow-y: auto; background: #f8f9fa; padding: 1rem; border-radius: 4px;">
{% for log in recent_logs %}
<div style="margin-bottom: 0.5rem; padding: 0.5rem; background: white; border-radius: 4px;">
<strong>{{ log.level }}</strong> - {{ log.message }}
<small style="color: #666;">({{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') if log.created_at else 'N/A' }})</small>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Logs - Fixture Manager</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.header {
background-color: #007bff;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
}
.nav {
display: flex;
gap: 1rem;
}
.nav a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.3s;
}
.nav a:hover {
background-color: rgba(255,255,255,0.1);
}
.container { max-width: 1200px; margin: 2rem auto; padding: 0 2rem; background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.btn { padding: 8px 16px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; display: inline-block; margin: 0.5rem 0; }
.log-container { max-height: 600px; overflow-y: auto; background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-top: 1rem; }
.log-entry { margin-bottom: 1rem; padding: 1rem; background: white; border-radius: 4px; border-left: 4px solid #007bff; }
.log-error { border-left-color: #dc3545; }
.log-warning { border-left-color: #ffc107; }
.log-info { border-left-color: #17a2b8; }
.log-header { font-weight: bold; margin-bottom: 0.5rem; }
.log-time { color: #666; font-size: 0.9rem; }
.log-message { margin-top: 0.5rem; }
</style>
</head>
<body>
<div class="header">
<div class="logo">🥊 Fixture Manager</div>
<div class="nav">
<a href="{{ url_for('main.dashboard') }}">Dashboard</a>
<a href="{{ url_for('main.fixtures') }}">Fixtures</a>
<a href="{{ url_for('main.uploads') }}">Uploads</a>
<a href="{{ url_for('main.statistics') }}">Statistics</a>
{% if current_user.is_admin %}
<a href="{{ url_for('main.admin_panel') }}">Admin</a>
{% endif %}
<a href="{{ url_for('auth.logout') }}">Logout</a>
</div>
</div>
<div class="container">
<h1>System Logs</h1>
<a href="{{ url_for('main.admin_panel') }}" class="btn">← Back to Admin Panel</a>
{% if logs %}
<div class="log-container">
{% for log in logs %}
<div class="log-entry log-{{ log.level.lower() if log.level else 'info' }}">
<div class="log-header">
<span style="text-transform: uppercase;">{{ log.level or 'INFO' }}</span>
{% if log.module %}
- {{ log.module }}
{% endif %}
</div>
<div class="log-time">{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') if log.created_at else 'N/A' }}</div>
<div class="log-message">{{ log.message or 'No message' }}</div>
</div>
{% endfor %}
</div>
{% else %}
<p>No logs found.</p>
{% endif %}
</div>
</body>
</html>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Fixture Manager</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.header {
background-color: #007bff;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
}
.nav {
display: flex;
gap: 1rem;
}
.nav a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.3s;
}
.nav a:hover {
background-color: rgba(255,255,255,0.1);
}
.container {
max-width: 1200px;
margin: 2rem auto;
padding: 0 2rem;
}
.welcome {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 2rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #007bff;
margin-bottom: 0.5rem;
}
.stat-label {
color: #666;
font-size: 0.9rem;
}
.recent-section {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 2rem;
}
.section-title {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 1rem;
color: #333;
}
.item-list {
list-style: none;
padding: 0;
margin: 0;
}
.item-list li {
padding: 0.5rem 0;
border-bottom: 1px solid #eee;
}
.item-list li:last-child {
border-bottom: none;
}
.alert {
padding: 12px;
margin-bottom: 1rem;
border-radius: 4px;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.btn {
display: inline-block;
padding: 8px 16px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
font-size: 0.9rem;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="header">
<div class="logo">🥊 Fixture Manager</div>
<div class="nav">
<a href="{{ url_for('main.dashboard') }}">Dashboard</a>
<a href="{{ url_for('main.fixtures') }}">Fixtures</a>
<a href="{{ url_for('main.uploads') }}">Uploads</a>
<a href="{{ url_for('main.statistics') }}">Statistics</a>
{% if current_user.is_admin %}
<a href="{{ url_for('main.admin_panel') }}">Admin</a>
{% endif %}
<a href="{{ url_for('auth.logout') }}">Logout</a>
</div>
</div>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="welcome">
<h1>Welcome back, {{ current_user.username }}!</h1>
<p>Here's an overview of your fixture management activity.</p>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">{{ user_matches or 0 }}</div>
<div class="stat-label">Your Matches</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ user_uploads or 0 }}</div>
<div class="stat-label">Your Uploads</div>
</div>
{% if current_user.is_admin and system_stats %}
<div class="stat-card">
<div class="stat-number">{{ system_stats.get('total_matches', 0) }}</div>
<div class="stat-label">Total System Matches</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ system_stats.get('total_users', 0) }}</div>
<div class="stat-label">Total Users</div>
</div>
{% endif %}
</div>
{% if recent_fixtures %}
<div class="recent-section">
<div class="section-title">Recent Fixtures</div>
<ul class="item-list">
{% for fixture in recent_fixtures %}
<li>
<strong>{{ fixture.filename }}</strong>:
{{ fixture.match_count }} matches
<span style="float: right;">
<a href="{{ url_for('main.fixture_detail', fixture_id=fixture.fixture_id) }}" class="btn">View</a>
</span>
</li>
{% endfor %}
</ul>
<div style="margin-top: 1rem;">
<a href="{{ url_for('main.fixtures') }}" class="btn">View All Fixtures</a>
</div>
</div>
{% endif %}
<div class="recent-section">
<div class="section-title">Quick Actions</div>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<a href="{{ url_for('upload.upload_fixture') }}" class="btn">Upload Fixture File</a>
<a href="{{ url_for('main.fixtures') }}" class="btn">Browse Fixtures</a>
<a href="{{ url_for('main.statistics') }}" class="btn">View Statistics</a>
</div>
</div>
</div>
</body>
</html>
\ No newline at end of file
This diff is collapsed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fixture Manager - Welcome</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
max-width: 500px;
width: 90%;
}
.logo {
font-size: 2.5rem;
color: #333;
margin-bottom: 1rem;
font-weight: bold;
}
.subtitle {
color: #666;
margin-bottom: 2rem;
font-size: 1.1rem;
}
.btn {
display: inline-block;
padding: 12px 24px;
margin: 0.5rem;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
transition: background-color 0.3s;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
}
.features {
margin-top: 2rem;
text-align: left;
}
.feature {
margin: 1rem 0;
padding: 0.5rem;
}
.feature-icon {
color: #007bff;
margin-right: 0.5rem;
}
</style>
</head>
<body>
<div class="container">
<div class="logo">🥊 Fixture Manager</div>
<div class="subtitle">Manage your football fixtures and match data efficiently</div>
<div>
<a href="{{ url_for('auth.login') }}" class="btn btn-primary">Login</a>
<a href="{{ url_for('auth.register') }}" class="btn btn-secondary">Register</a>
</div>
<div class="features">
<div class="feature">
<span class="feature-icon">📊</span>
<strong>Match Management:</strong> Upload and manage fixture data
</div>
<div class="feature">
<span class="feature-icon">📁</span>
<strong>File Uploads:</strong> Support for CSV and Excel files
</div>
<div class="feature">
<span class="feature-icon">📈</span>
<strong>Statistics:</strong> Track match outcomes and performance
</div>
<div class="feature">
<span class="feature-icon">🔒</span>
<strong>Secure:</strong> User authentication and data protection
</div>
</div>
</div>
</body>
</html>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Statistics - Fixture Manager</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.header {
background-color: #007bff;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
}
.nav {
display: flex;
gap: 1rem;
}
.nav a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.3s;
}
.nav a:hover {
background-color: rgba(255,255,255,0.1);
}
.container {
max-width: 1200px;
margin: 2rem auto;
padding: 0 2rem;
}
.content {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.btn {
padding: 8px 16px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
display: inline-block;
margin: 0.5rem 0;
}
.btn:hover {
background-color: #0056b3;
}
.alert {
padding: 12px;
margin-bottom: 1rem;
border-radius: 4px;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin: 1rem 0;
}
.stat-card {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
text-align: center;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #007bff;
}
.stat-label {
color: #666;
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="header">
<div class="logo">🥊 Fixture Manager</div>
<div class="nav">
<a href="{{ url_for('main.dashboard') }}">Dashboard</a>
<a href="{{ url_for('main.fixtures') }}">Fixtures</a>
<a href="{{ url_for('main.uploads') }}">Uploads</a>
<a href="{{ url_for('main.statistics') }}">Statistics</a>
{% if current_user.is_admin %}
<a href="{{ url_for('main.admin_panel') }}">Admin</a>
{% endif %}
<a href="{{ url_for('auth.logout') }}">Logout</a>
</div>
</div>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="content">
<h1>Statistics</h1>
<a href="{{ url_for('main.dashboard') }}" class="btn">← Back to Dashboard</a>
<h2>Your Statistics</h2>
<div class="stats-grid">
{% if user_stats %}
<div class="stat-card">
<div class="stat-number">{{ user_stats.get('total_uploads', 0) }}</div>
<div class="stat-label">Total Uploads</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ user_stats.get('total_matches', 0) }}</div>
<div class="stat-label">Total Matches</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ user_stats.get('active_matches', 0) }}</div>
<div class="stat-label">Active Matches</div>
</div>
{% endif %}
</div>
{% if current_user.is_admin and system_stats %}
<h2>System Statistics</h2>
<div class="stats-grid">
{% for key, value in system_stats.items() %}
<div class="stat-card">
<div class="stat-number">{{ value }}</div>
<div class="stat-label">{{ key.replace('_', ' ').title() }}</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Uploads - Fixture Manager</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.header {
background-color: #007bff;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
}
.nav {
display: flex;
gap: 1rem;
}
.nav a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.3s;
}
.nav a:hover {
background-color: rgba(255,255,255,0.1);
}
.container {
max-width: 1200px;
margin: 2rem auto;
padding: 0 2rem;
}
.content {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.btn {
padding: 8px 16px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
display: inline-block;
margin: 0.5rem 0;
}
.btn:hover {
background-color: #0056b3;
}
.alert {
padding: 12px;
margin-bottom: 1rem;
border-radius: 4px;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f8f9fa;
font-weight: bold;
}
</style>
</head>
<body>
<div class="header">
<div class="logo">🥊 Fixture Manager</div>
<div class="nav">
<a href="{{ url_for('main.dashboard') }}">Dashboard</a>
<a href="{{ url_for('main.fixtures') }}">Fixtures</a>
<a href="{{ url_for('main.uploads') }}">Uploads</a>
<a href="{{ url_for('main.statistics') }}">Statistics</a>
{% if current_user.is_admin %}
<a href="{{ url_for('main.admin_panel') }}">Admin</a>
{% endif %}
<a href="{{ url_for('auth.logout') }}">Logout</a>
</div>
</div>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="content">
<h1>File Uploads</h1>
<a href="{{ url_for('main.dashboard') }}" class="btn">← Back to Dashboard</a>
<a href="{{ url_for('upload.upload_fixture') }}" class="btn">Upload New File</a>
{% if uploads %}
<table>
<thead>
<tr><th>Filename</th><th>Type</th><th>Status</th><th>Upload Date</th></tr>
</thead>
<tbody>
{% for upload in uploads %}
<tr>
<td>{{ upload.filename }}</td>
<td>{{ upload.file_type }}</td>
<td>{{ upload.upload_status }}</td>
<td>{{ upload.created_at.strftime('%Y-%m-%d %H:%M') if upload.created_at else 'N/A' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No uploads found.</p>
{% endif %}
</div>
</div>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Database Setup - Fixture Manager</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.step {
background: #f8f9fa;
border-left: 4px solid #007bff;
padding: 15px;
margin: 15px 0;
}
.step h3 {
margin-top: 0;
color: #007bff;
}
.code {
background: #2d3748;
color: #e2e8f0;
padding: 15px;
border-radius: 5px;
font-family: 'Courier New', monospace;
overflow-x: auto;
margin: 10px 0;
}
.success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
padding: 15px;
border-radius: 5px;
margin: 15px 0;
}
.btn {
background: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
display: inline-block;
margin: 10px 5px;
}
.btn:hover {
background: #0056b3;
}
.config-example {
background: #f1f3f4;
border: 1px solid #dadce0;
padding: 15px;
border-radius: 5px;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔧 Database Setup Required</h1>
<p>The Fixture Manager needs a MySQL database to function properly.</p>
</div>
<div class="warning">
<strong>⚠️ Database Not Connected</strong><br>
The application cannot connect to the database. Please follow the steps below to set up your database.
</div>
<div class="step">
<h3>Step 1: Install MySQL Server</h3>
<p>If you don't have MySQL installed, install it first:</p>
<div class="code">
# Ubuntu/Debian
sudo apt update
sudo apt install mysql-server
# CentOS/RHEL/Fedora
sudo yum install mysql-server
# or
sudo dnf install mysql-server
# Start MySQL service
sudo systemctl start mysql
sudo systemctl enable mysql
</div>
</div>
<div class="step">
<h3>Step 2: Create Database and User</h3>
<p>Log into MySQL and create the database:</p>
<div class="code">
# Log into MySQL as root
sudo mysql -u root -p
# Create database
CREATE DATABASE fixture_manager CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
# Create user (replace 'your_password' with a secure password)
CREATE USER 'fixture_user'@'localhost' IDENTIFIED BY 'your_password';
# Grant privileges
GRANT ALL PRIVILEGES ON fixture_manager.* TO 'fixture_user'@'localhost';
FLUSH PRIVILEGES;
# Exit MySQL
EXIT;
</div>
</div>
<div class="step">
<h3>Step 3: Configure Environment Variables</h3>
<p>Create a <code>.env</code> file in the application directory with your database settings:</p>
<div class="config-example">
<strong>Example .env file:</strong>
<div class="code">
# Database Configuration
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USER=fixture_user
MYSQL_PASSWORD=your_password
MYSQL_DATABASE=fixture_manager
# Security
SECRET_KEY=your-secret-key-here
JWT_SECRET_KEY=your-jwt-secret-key-here
# Application Settings
HOST=0.0.0.0
PORT=5000
DEBUG=False
LOG_LEVEL=INFO
# File Upload Settings
UPLOAD_FOLDER=/var/lib/fixture-daemon/uploads
MAX_CONTENT_LENGTH=524288000
# Daemon Settings
DAEMON_PID_FILE=/var/run/fixture-daemon.pid
DAEMON_LOG_FILE=/var/log/fixture-daemon.log
DAEMON_WORKING_DIR=/var/lib/fixture-daemon
</div>
</div>
</div>
<div class="step">
<h3>Step 4: Initialize Database Schema</h3>
<p>The application will automatically create the necessary tables when it connects to the database.</p>
<p>Alternatively, you can manually run the schema file:</p>
<div class="code">
mysql -u fixture_user -p fixture_manager < database/schema.sql
</div>
</div>
<div class="step">
<h3>Step 5: Restart the Application</h3>
<p>After configuring the database, restart the Fixture Manager daemon:</p>
<div class="code">
# Stop the daemon
./fixture-manager stop
# Start the daemon
./fixture-manager start
# Or restart
./fixture-manager restart
</div>
</div>
<div class="success">
<strong>✅ Default Admin Account</strong><br>
Once the database is set up, a default admin account will be created:<br>
<strong>Username:</strong> admin<br>
<strong>Password:</strong> admin123<br>
<em>Please change this password immediately after first login!</em>
</div>
<div style="text-align: center; margin-top: 30px;">
<a href="/" class="btn">🔄 Check Connection</a>
<a href="/health" class="btn">📊 System Health</a>
</div>
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px;">
<p><strong>Need Help?</strong></p>
<ul>
<li>Check the application logs: <code>tail -f /var/log/fixture-daemon.log</code></li>
<li>Verify MySQL is running: <code>sudo systemctl status mysql</code></li>
<li>Test database connection: <code>mysql -u fixture_user -p fixture_manager</code></li>
</ul>
</div>
</div>
</body>
</html>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import pandas as pd import pandas as pd
import logging import logging
import re import re
import uuid
from datetime import datetime from datetime import datetime
from typing import Dict, List, Tuple, Optional from typing import Dict, List, Tuple, Optional
from app import db from app import db
...@@ -343,6 +344,10 @@ class FixtureParser: ...@@ -343,6 +344,10 @@ class FixtureParser:
try: try:
saved_match_ids = [] saved_match_ids = []
# Generate a single fixture_id for all matches from this file upload
fixture_id = str(uuid.uuid4())
logger.info(f"Generated fixture_id {fixture_id} for {len(parsed_matches)} matches")
for match_data in parsed_matches: for match_data in parsed_matches:
try: try:
# Check if match number already exists # Check if match number already exists
...@@ -351,7 +356,7 @@ class FixtureParser: ...@@ -351,7 +356,7 @@ class FixtureParser:
logger.warning(f"Match number {match_data['match_number']} already exists, skipping") logger.warning(f"Match number {match_data['match_number']} already exists, skipping")
continue continue
# Create match record # Create match record with shared fixture_id
match = Match( match = Match(
match_number=match_data['match_number'], match_number=match_data['match_number'],
fighter1_township=match_data['fighter1_township'], fighter1_township=match_data['fighter1_township'],
...@@ -359,6 +364,7 @@ class FixtureParser: ...@@ -359,6 +364,7 @@ class FixtureParser:
venue_kampala_township=match_data['venue_kampala_township'], venue_kampala_township=match_data['venue_kampala_township'],
filename=match_data['filename'], filename=match_data['filename'],
file_sha1sum=file_sha1sum, file_sha1sum=file_sha1sum,
fixture_id=fixture_id, # Use shared fixture_id
created_by=match_data['created_by'] created_by=match_data['created_by']
) )
...@@ -395,6 +401,7 @@ class FixtureParser: ...@@ -395,6 +401,7 @@ class FixtureParser:
def get_parsing_statistics(self) -> Dict: def get_parsing_statistics(self) -> Dict:
"""Get fixture parsing statistics""" """Get fixture parsing statistics"""
try: try:
from app.models import Match, MatchOutcome
stats = { stats = {
'total_matches': Match.query.count(), 'total_matches': Match.query.count(),
'active_matches': Match.query.filter_by(active_status=True).count(), 'active_matches': Match.query.filter_by(active_status=True).count(),
......
This diff is collapsed.
This diff is collapsed.
...@@ -24,7 +24,7 @@ class Config: ...@@ -24,7 +24,7 @@ class Config:
} }
# File Upload Configuration # File Upload Configuration
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER') or '/var/lib/fixture-daemon/uploads' UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER') or os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
MAX_CONTENT_LENGTH = int(os.environ.get('MAX_CONTENT_LENGTH') or 500 * 1024 * 1024) # 500MB MAX_CONTENT_LENGTH = int(os.environ.get('MAX_CONTENT_LENGTH') or 500 * 1024 * 1024) # 500MB
ALLOWED_FIXTURE_EXTENSIONS = {'csv', 'xlsx', 'xls'} ALLOWED_FIXTURE_EXTENSIONS = {'csv', 'xlsx', 'xls'}
ALLOWED_ZIP_EXTENSIONS = {'zip'} ALLOWED_ZIP_EXTENSIONS = {'zip'}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
#!/bin/bash
# Fixture Manager Daemon Runner
# Set executable permissions
chmod +x ./fixture-manager
# Run the daemon
./fixture-manager "$@"
This diff is collapsed.
This diff is collapsed.
from PyInstaller.utils.hooks import collect_all
datas, binaries, hiddenimports = collect_all('flask_sqlalchemy')
hiddenimports += [
'sqlalchemy.dialects.mysql',
'sqlalchemy.dialects.mysql.pymysql',
'sqlalchemy.pool',
'sqlalchemy.engine.default',
]
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment