# encoding=utf-8 import pymysql import os import json import hashlib, uuid import datetime from datetime import datetime # Load database configuration first from db_setup import db_config_manager db_config_manager.load_config() db_config_manager._update_environment_variables() # Reload database modules to pick up new environment variables import importlib import database import db_config importlib.reload(database) importlib.reload(db_config) from app import app, randomUrlSuffix from flask import Flask, flash, render_template, request, redirect, url_for, jsonify from sqlalchemy import text from flask_wtf import FlaskForm from flask_bootstrap import Bootstrap from flask_basicauth import BasicAuth from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms import DateField from wtforms.validators import InputRequired, Email, Length, EqualTo from forms import motmForm, adminSettingsForm2, goalsAssistsForm, DatabaseSetupForm, PlayerForm, ClubForm, TeamForm, DataImportForm, ClubSelectionForm, S3ConfigForm from db_config import sql_write, sql_write_static, sql_read, sql_read_static from sqlalchemy import text from tables import matchSquadTable from readSettings import mySettings from fixture_scraper import FixtureScraper, get_next_hkfc_c_fixture, get_opponent_club_name, get_opponent_club_info, match_opponent_to_club from club_scraper import ClubScraper, get_hk_hockey_clubs, expand_club_abbreviation from s3_config import s3_config_manager, s3_asset_service # Custom authentication class that uses database class DatabaseBasicAuth(BasicAuth): def check_credentials(self, username, password): try: sql = text("SELECT password_hash FROM admin_profiles WHERE username = :username") result = sql_read(sql, {'username': username}) if result: stored_hash = result[0]['password_hash'] password_hash = hashlib.sha256(password.encode()).hexdigest() return password_hash == stored_hash return False except Exception as e: print(f"Authentication error: {e}") return False app.config['BASIC_AUTH_USERNAME'] = 'admin' # Fallback for compatibility app.config['BASIC_AUTH_PASSWORD'] = 'letmein' # Fallback for compatibility basic_auth = DatabaseBasicAuth(app) # Admin profile form class AdminProfileForm(FlaskForm): current_password = PasswordField('Current Password', validators=[InputRequired()]) new_password = PasswordField('New Password', validators=[InputRequired(), Length(min=6)]) confirm_password = PasswordField('Confirm New Password', validators=[InputRequired(), EqualTo('new_password', message='Passwords must match')]) email = StringField('Email', validators=[Email()]) submit = SubmitField('Update Profile') @app.route('/') def index(): """Main index page for MOTM system""" # Check if user is authenticated as admin is_admin = is_admin_authenticated(request) return render_template('index.html', is_admin=is_admin) @app.route('/admin') @basic_auth.required def admin_dashboard(): """Admin dashboard - central hub for all admin functions""" return render_template('admin_dashboard.html') @app.route('/admin/profile', methods=['GET', 'POST']) @basic_auth.required def admin_profile(): """Admin profile page for changing password and settings""" form = AdminProfileForm() # Get current admin profile sql = text("SELECT username, email FROM admin_profiles WHERE username = 'admin'") profile = sql_read(sql) if profile: current_email = profile[0]['email'] else: current_email = '' if request.method == 'POST' and form.validate_on_submit(): # Verify current password sql_check = text("SELECT password_hash FROM admin_profiles WHERE username = 'admin'") result = sql_read(sql_check) if result: stored_hash = result[0]['password_hash'] current_password_hash = hashlib.sha256(form.current_password.data.encode()).hexdigest() if current_password_hash == stored_hash: # Update password and email new_password_hash = hashlib.sha256(form.new_password.data.encode()).hexdigest() sql_update = text(""" UPDATE admin_profiles SET password_hash = :password_hash, email = :email, updated_at = CURRENT_TIMESTAMP WHERE username = 'admin' """) sql_write(sql_update, { 'password_hash': new_password_hash, 'email': form.email.data }) flash('Profile updated successfully!', 'success') return redirect(url_for('admin_profile')) else: flash('Current password is incorrect!', 'error') else: flash('Admin profile not found!', 'error') # Pre-populate email field form.email.data = current_email return render_template('admin_profile.html', form=form, current_email=current_email) def generate_device_id(request): """Generate a device identifier from request headers""" import hashlib # Collect device characteristics user_agent = request.headers.get('User-Agent', '') accept_language = request.headers.get('Accept-Language', '') accept_encoding = request.headers.get('Accept-Encoding', '') ip_address = request.environ.get('REMOTE_ADDR', '') # Create a fingerprint from these characteristics fingerprint_string = f"{user_agent}|{accept_language}|{accept_encoding}|{ip_address}" device_id = hashlib.sha256(fingerprint_string.encode()).hexdigest()[:16] # Use first 16 chars return device_id def is_admin_authenticated(request): """Check if the current request is authenticated as admin""" try: # Check if Authorization header exists auth_header = request.headers.get('Authorization', '') if not auth_header.startswith('Basic '): return False # Decode the basic auth credentials import base64 encoded_credentials = auth_header[6:] # Remove 'Basic ' prefix try: credentials = base64.b64decode(encoded_credentials).decode('utf-8') username, password = credentials.split(':', 1) # Check against database sql = text("SELECT password_hash FROM admin_profiles WHERE username = :username") result = sql_read(sql, {'username': username}) if result: stored_hash = result[0]['password_hash'] password_hash = hashlib.sha256(password.encode()).hexdigest() return password_hash == stored_hash except: pass except: pass return False def get_previous_match_winners(): """ Automatically determine the MOTM and DotD winners from the most recent completed fixture. Returns a tuple of (motm_player_number, dotd_player_number) or (None, None) if not found. """ try: # Get all fixture columns from _hkfc_c_motm table sql_columns = text(""" SELECT column_name FROM information_schema.columns WHERE table_name = '_hkfc_c_motm' AND (column_name LIKE 'motm_%' OR column_name LIKE 'dotd_%') AND column_name NOT LIKE '%total' ORDER BY column_name DESC """) columns = sql_read(sql_columns) if not columns: return None, None # Extract unique fixture dates from column names fixture_dates = set() for col in columns: col_name = col['column_name'] if col_name.startswith('motm_'): fixture_dates.add(col_name.replace('motm_', '')) elif col_name.startswith('dotd_'): fixture_dates.add(col_name.replace('dotd_', '')) # Sort fixture dates in descending order (most recent first) sorted_dates = sorted(list(fixture_dates), reverse=True) if not sorted_dates: return None, None # Get the most recent fixture date latest_fixture = sorted_dates[0] motm_col = f'motm_{latest_fixture}' dotd_col = f'dotd_{latest_fixture}' # Find the MOTM winner (player with most votes) sql_motm = text(f""" SELECT playernumber, {motm_col} as votes FROM _hkfc_c_motm WHERE {motm_col} > 0 ORDER BY {motm_col} DESC LIMIT 1 """) motm_result = sql_read(sql_motm) motm_winner = motm_result[0]['playernumber'] if motm_result else None # Find the DotD winner (player with most votes) sql_dotd = text(f""" SELECT playernumber, {dotd_col} as votes FROM _hkfc_c_motm WHERE {dotd_col} > 0 ORDER BY {dotd_col} DESC LIMIT 1 """) dotd_result = sql_read(sql_dotd) dotd_winner = dotd_result[0]['playernumber'] if dotd_result else None return motm_winner, dotd_winner except Exception as e: print(f"Error getting previous match winners: {e}") return None, None # ==================== PUBLIC VOTING SECTION ==================== @app.route('/motm/') def motm_vote(randomUrlSuffix): """Public voting page for Man of the Match and Dick of the Day""" sql = text("SELECT playernumber, playerforenames, playersurname, playernickname FROM _hkfcc_matchsquad ORDER BY RANDOM()") sql2 = text("SELECT nextclub, nextteam, nextdate, oppologo, hkfclogo, currmotm, currdotd, nextfixture, motmurlsuffix, votingdeadline FROM motmadminsettings") rows = sql_read(sql) nextInfo = sql_read_static(sql2) # Handle empty results if not nextInfo: return render_template('error.html', message="Database not initialized. Please go to Database Setup to initialize the database.") nextClub = nextInfo[0]['nextclub'] nextTeam = nextInfo[0]['nextteam'] nextFixture = nextInfo[0]['nextfixture'] # Get HKFC logo from clubs table using signed URLs (with authentication) hkfcLogo = s3_asset_service.get_asset_url('images/hkfclogo.png') # Default fallback sql_hkfclogo = text("SELECT logo_url FROM clubs WHERE hockey_club = 'Hong Kong Football Club'") hkfclogo_result = sql_read(sql_hkfclogo) if hkfclogo_result and hkfclogo_result[0]['logo_url']: hkfcLogo = s3_asset_service.get_logo_url(hkfclogo_result[0]['logo_url'], 'Hong Kong Football Club') # Get opponent club logo from clubs table using signed URLs (with authentication) oppoLogo = s3_asset_service.get_asset_url('images/default_logo.png') # Default fallback if nextClub: sql_club_logo = text("SELECT logo_url FROM clubs WHERE hockey_club = :club_name") club_logo_result = sql_read(sql_club_logo, {'club_name': nextClub}) if club_logo_result and club_logo_result[0]['logo_url']: oppoLogo = s3_asset_service.get_logo_url(club_logo_result[0]['logo_url'], nextClub) oppo = nextTeam # Get match date from admin settings if nextInfo and nextInfo[0]['nextdate']: nextDate = nextInfo[0]['nextdate'] if isinstance(nextDate, str): nextDate = datetime.strptime(nextDate, '%Y-%m-%d') formatDate = nextDate.strftime('%A, %d %B %Y') else: return render_template('error.html', message="No match date found. Please set up the next match in admin settings.") # Get current MOTM and DotD player nicknames (if they exist) currMotM = None currDotD = None if nextInfo and nextInfo[0]['currmotm']: sql3 = text("SELECT playernickname FROM _hkfc_players WHERE playernumber = :currmotm") motm_result = sql_read(sql3, {'currmotm': nextInfo[0]['currmotm']}) if motm_result: currMotM = motm_result[0]['playernickname'] if nextInfo and nextInfo[0]['currdotd']: sql4 = text("SELECT playernickname FROM _hkfc_players WHERE playernumber = :currdotd") dotd_result = sql_read(sql4, {'currdotd': nextInfo[0]['currdotd']}) if dotd_result: currDotD = dotd_result[0]['playernickname'] # Use default player images since playerPictureURL column doesn't exist motmURL = s3_asset_service.get_asset_url('images/default_player.png') dotdURL = s3_asset_service.get_asset_url('images/default_player.png') # Get match comments sql5 = text("SELECT comment FROM _motmcomments WHERE matchDate = :match_date ORDER BY RANDOM() LIMIT 1") comment_result = sql_read(sql5, {'match_date': nextInfo[0]['nextdate'].strftime('%Y-%m-%d')}) comment = comment_result[0]['comment'] if comment_result else "No comments added yet" form = motmForm() # Get voting deadline voting_deadline = None voting_deadline_iso = None if nextInfo and nextInfo[0].get('votingdeadline'): voting_deadline = nextInfo[0]['votingdeadline'] if isinstance(voting_deadline, str): try: from datetime import datetime voting_deadline = datetime.strptime(voting_deadline, '%Y-%m-%d %H:%M:%S') except ValueError: pass if hasattr(voting_deadline, 'isoformat'): voting_deadline_iso = voting_deadline.isoformat() # Verify URL suffix if nextInfo and nextInfo[0].get('motmurlsuffix'): randomSuff = nextInfo[0]['motmurlsuffix'] if randomSuff == randomUrlSuffix: # Use nextdate to generate proper match number instead of nextfixture match_number = nextInfo[0]['nextdate'].strftime('%Y-%m-%d') if nextInfo[0]['nextdate'] else '' return render_template('motm_vote.html', data=rows, comment=comment, formatDate=formatDate, matchNumber=match_number, oppo=oppo, hkfcLogo=hkfcLogo, oppoLogo=oppoLogo, dotdURL=dotdURL, motmURL=motmURL, currMotM=currMotM, currDotD=currDotD, form=form, votingDeadline=voting_deadline_iso) else: return render_template('error.html', message="Invalid voting URL. Please use the correct URL provided by the admin.") else: return render_template('error.html', message="Voting not activated. Please contact the admin to activate voting.") @app.route('/motm/comments', methods=['GET', 'POST']) def match_comments(): """Display and allow adding match comments""" sql = text("SELECT nextclub, nextteam, nextdate, oppologo, hkfclogo FROM motmadminsettings") row = sql_read_static(sql) if not row: return render_template('error.html', message="Database not initialized. Please go to Database Setup to initialize the database.") _oppo = row[0]['nextclub'] # Handle case where nextdate is None - use most recent comment date if row[0]['nextdate']: commentDate = row[0]['nextdate'].strftime('%Y-%m-%d') _matchDate = row[0]['nextdate'].strftime('%Y-%m-%d') else: # Get the most recent comment date sql_recent = text("SELECT matchDate FROM _motmcomments ORDER BY matchDate DESC LIMIT 1") recent_result = sql_read(sql_recent) if recent_result: commentDate = recent_result[0]['matchDate'] _matchDate = recent_result[0]['matchDate'] else: commentDate = '2025-01-15' # Fallback _matchDate = '2025-01-15' # Get HKFC logo from clubs table using signed URLs (with authentication) hkfcLogo = s3_asset_service.get_asset_url('images/hkfclogo.png') # Default fallback sql_hkfclogo = text("SELECT logo_url FROM clubs WHERE hockey_club = 'Hong Kong Football Club'") hkfclogo_result = sql_read(sql_hkfclogo) if hkfclogo_result and hkfclogo_result[0]['logo_url']: hkfcLogo = s3_asset_service.get_logo_url(hkfclogo_result[0]['logo_url'], 'Hong Kong Football Club') # Get opponent club logo from clubs table using signed URLs (with authentication) oppoLogo = s3_asset_service.get_asset_url('images/default_logo.png') # Default fallback if _oppo: sql_club_logo = text("SELECT logo_url FROM clubs WHERE hockey_club = :club_name") club_logo_result = sql_read(sql_club_logo, {'club_name': _oppo}) if club_logo_result and club_logo_result[0]['logo_url']: oppoLogo = s3_asset_service.get_logo_url(club_logo_result[0]['logo_url'], _oppo) if request.method == 'POST': _comment = request.form['matchComment'] if _comment != 'Optional comments added here': _fixed_comment = _comment.replace("'", "\\'") sql3 = text("INSERT INTO _motmcomments (matchDate, comment) VALUES (:comment_date, :comment)") sql_write(sql3, {'comment_date': commentDate, 'comment': _fixed_comment}) sql = text("SELECT comment FROM _motmcomments WHERE matchDate = :match_date ORDER BY RANDOM()") comments = sql_read(sql, {'match_date': _matchDate}) return render_template('match_comments.html', comments=comments, hkfcLogo=hkfcLogo, oppoLogo=oppoLogo) @app.route('/motm/vote-thanks', methods=['POST']) def vote_thanks(): """Process MOTM/DotD votes and comments""" try: # Check voting deadline sql_deadline = text("SELECT votingdeadline FROM motmadminsettings") deadline_result = sql_read_static(sql_deadline) if deadline_result and deadline_result[0].get('votingdeadline'): from datetime import datetime voting_deadline = deadline_result[0]['votingdeadline'] if isinstance(voting_deadline, str): try: voting_deadline = datetime.strptime(voting_deadline, '%Y-%m-%d %H:%M:%S') except ValueError: pass # Check if deadline has passed if hasattr(voting_deadline, 'year'): # Check if it's a datetime object if datetime.now() >= voting_deadline: return render_template('error.html', message="Voting has closed. The deadline has passed.") _motm = request.form['motmVote'] _dotd = request.form['dotdVote'] _comments = request.form['motmComment'] _fixed_comments = _comments.replace("'", "\\'") _matchDate = request.form['matchNumber'] _oppo = request.form['oppo'] if _motm and _dotd and request.method == 'POST': # Check if fixture columns exist, create them if not fixture_date = _matchDate.replace('-', '') motm_col = f'motm_{fixture_date}' dotd_col = f'dotd_{fixture_date}' # Create columns if they don't exist try: sql_create_motm = text(f"ALTER TABLE _hkfc_c_motm ADD COLUMN {motm_col} INTEGER DEFAULT 0") sql_write(sql_create_motm) except: pass # Column already exists try: sql_create_dotd = text(f"ALTER TABLE _hkfc_c_motm ADD COLUMN {dotd_col} INTEGER DEFAULT 0") sql_write(sql_create_dotd) except: pass # Column already exists # Get player names motm_player = sql_read(text("SELECT playernickname FROM _hkfc_players WHERE playernumber = :player_num"), {'player_num': _motm}) dotd_player = sql_read(text("SELECT playernickname FROM _hkfc_players WHERE playernumber = :player_num"), {'player_num': _dotd}) motm_name = motm_player[0]['playernickname'] if motm_player else f'Player {_motm}' dotd_name = dotd_player[0]['playernickname'] if dotd_player else f'Player {_dotd}' # Update MOTM vote - use PostgreSQL UPSERT syntax sql_motm = text(f""" INSERT INTO _hkfc_c_motm (playernumber, playername, {motm_col}) VALUES (:player_num, :player_name, 1) ON CONFLICT (playernumber) DO UPDATE SET {motm_col} = _hkfc_c_motm.{motm_col} + 1 """) sql_write(sql_motm, {'player_num': _motm, 'player_name': motm_name}) # Update DotD vote - use PostgreSQL UPSERT syntax sql_dotd = text(f""" INSERT INTO _hkfc_c_motm (playernumber, playername, {dotd_col}) VALUES (:player_num, :player_name, 1) ON CONFLICT (playernumber) DO UPDATE SET {dotd_col} = _hkfc_c_motm.{dotd_col} + 1 """) sql_write(sql_dotd, {'player_num': _dotd, 'player_name': dotd_name}) # Recalculate totals for both players def update_player_totals(player_num): # Get player data sql_player = text("SELECT * FROM _hkfc_c_motm WHERE playernumber = :player_num") player_data = sql_read(sql_player, {'player_num': player_num}) if player_data: player = player_data[0] motm_total = 0 dotd_total = 0 # Calculate totals from fixture columns for col_name in player.keys(): if col_name.startswith('motm_') and col_name != 'motmtotal': motm_total += player[col_name] or 0 elif col_name.startswith('dotd_') and col_name != 'dotdtotal': dotd_total += player[col_name] or 0 # Update stored totals sql_update = text("UPDATE _hkfc_c_motm SET motmtotal = :motm_total, dotdtotal = :dotd_total WHERE playernumber = :player_num") sql_write(sql_update, {'motm_total': motm_total, 'dotd_total': dotd_total, 'player_num': player_num}) # Update totals for both players update_player_totals(_motm) update_player_totals(_dotd) # Generate device identifier and record vote for tracking device_id = generate_device_id(request) sql_device = text(""" INSERT INTO device_votes (device_id, fixture_date, motm_player_number, dotd_player_number, motm_player_name, dotd_player_name, ip_address, user_agent) VALUES (:device_id, :fixture_date, :motm_player, :dotd_player, :motm_name, :dotd_name, :ip_address, :user_agent) """) sql_write(sql_device, { 'device_id': device_id, 'fixture_date': fixture_date, 'motm_player': _motm, 'dotd_player': _dotd, 'motm_name': motm_name, 'dotd_name': dotd_name, 'ip_address': request.environ.get('REMOTE_ADDR', 'unknown'), 'user_agent': request.headers.get('User-Agent', 'unknown') }) # Handle comments if _comments and _comments != "Optional comments added here": sql3 = text("INSERT INTO _motmcomments (matchDate, comment) VALUES (:match_date, :comment)") sql_write(sql3, {'match_date': _matchDate, 'comment': _fixed_comments}) # Get Simpsons monkeys image URL with fallback try: # First try to get from S3 simpsons_url = s3_asset_service.get_asset_url('images/simpsons-monkeys.jpg') print(f"DEBUG: Simpsons image URL: {simpsons_url}") # If S3 is disabled or URL is fallback, use static if simpsons_url.startswith('/static/'): print("DEBUG: Using fallback static URL") else: print("DEBUG: Using S3 URL") except Exception as e: print(f"DEBUG: Error getting Simpsons image: {e}") # Fallback to static URL simpsons_url = "/static/images/simpsons-monkeys.jpg" return render_template('vote_thanks.html', simpsons_image_url=simpsons_url) else: return 'Ouch ... something went wrong here' except Exception as e: print(e) finally: print('Votes cast') # ==================== ADMIN SECTION ==================== @app.route('/admin/motm', methods=['GET', 'POST']) @basic_auth.required def motm_admin(): """Admin page for managing MOTM settings""" form = adminSettingsForm2() prevFixture = mySettings('prevFixture') if prevFixture is None: prevFixture = '1' else: prevFixture = str(prevFixture) if request.method == 'POST': print(f"DEBUG: POST request received") print(f"DEBUG: form.saveButton.data = {form.saveButton.data}") print(f"DEBUG: form.activateButton.data = {form.activateButton.data}") if form.saveButton.data: print('DEBUG: Save button clicked') else: print('DEBUG: Activate button clicked') _nextTeam = request.form.get('nextOppoTeam', '') _nextMatchDate = request.form.get('nextMatchDate', '') _votingDeadline = request.form.get('votingDeadline', '') _currMotM = request.form.get('currMotM', '0') _currDotD = request.form.get('currDotD', '0') print(f"DEBUG: Form data - team: {_nextTeam}, date: {_nextMatchDate}") # Validate required fields if not _nextTeam or not _nextMatchDate: flash('Error: Match date and opposition team are required', 'error') return redirect(url_for('motm_admin')) # Use the opponent matching system to find the correct club opponent_club_info = get_opponent_club_info(_nextTeam) if opponent_club_info and opponent_club_info.get('club_name'): _nextClub = opponent_club_info['club_name'] else: # Fallback to old method for backward compatibility sql1 = "SELECT club FROM _clubTeams WHERE displayName='" + _nextTeam + "'" _nextClubName = sql_read_static(sql1) if not _nextClubName: flash('Error: Club not found for team ' + _nextTeam + '. Please ensure the club exists in the club database.', 'error') return redirect(url_for('motm_admin')) _nextClub = _nextClubName[0]['club'] # Get the form values for previous MOTM and DotD # If user selected '0' (No Previous), use None # Otherwise use the selected player number currmotm_value = None if _currMotM == '0' else _currMotM currdotd_value = None if _currDotD == '0' else _currDotD # Parse voting deadline if provided voting_deadline_value = None if _votingDeadline: try: from datetime import datetime voting_deadline_value = datetime.strptime(_votingDeadline, '%Y-%m-%dT%H:%M') except ValueError: flash('Warning: Invalid voting deadline format. Deadline not set.', 'warning') sql = text("UPDATE motmadminsettings SET nextdate = :nextdate, nextclub = :nextclub, nextteam = :nextteam, currmotm = :currmotm, currdotd = :currdotd, votingdeadline = :voting_deadline") sql_write_static(sql, { 'nextdate': _nextMatchDate, 'nextclub': _nextClub, 'nextteam': _nextTeam, 'currmotm': currmotm_value, 'currdotd': currdotd_value, 'voting_deadline': voting_deadline_value }) # Update the opponent logo using the matched club information if opponent_club_info and opponent_club_info.get('logo_url'): # Use the logo URL from the matched club logo_url = opponent_club_info['logo_url'] sql2 = text("UPDATE motmadminsettings SET oppologo = :logo_url") sql_write_static(sql2, {'logo_url': logo_url}) else: # Fallback to old method sql2 = text("UPDATE motmadminsettings SET oppologo = (SELECT logo FROM menshockeyclubs WHERE hockeyclub = :nextclub) WHERE nextclub = :nextclub") sql_write_static(sql2, {'nextclub': _nextClub}) if form.saveButton.data: flash('Settings saved!') urlSuffix = randomUrlSuffix(8) print(f"DEBUG: Generated URL suffix: {urlSuffix}") # Check if row exists before update check_row = sql_read_static(text("SELECT userid, motmurlsuffix FROM motmadminsettings")) print(f"DEBUG: Rows in motmadminsettings: {check_row}") # Try to update with WHERE userid = 'admin' sql3 = text("UPDATE motmadminsettings SET motmurlsuffix = :url_suffix") print(f"DEBUG: About to execute UPDATE query with WHERE userid='admin'") result = sql_write_static(sql3, {'url_suffix': urlSuffix}) print(f"DEBUG: UPDATE query result: {result}") # Verify the update verify = sql_read_static(text("SELECT motmurlsuffix FROM motmadminsettings")) print(f"DEBUG: Verification with WHERE userid='admin': {verify}") # If the update didn't work, try updating all rows (if there's only one row) if not verify or not verify[0]['motmurlsuffix']: print(f"DEBUG: First UPDATE didn't work, trying UPDATE without WHERE clause") sql3_all = text("UPDATE motmadminsettings SET motmurlsuffix = :url_suffix") result_all = sql_write_static(sql3_all, {'url_suffix': urlSuffix}) print(f"DEBUG: UPDATE all rows result: {result_all}") # Verify again verify_all = sql_read_static(text("SELECT userid, motmurlsuffix FROM motmadminsettings")) print(f"DEBUG: Verification after UPDATE all: {verify_all}") if verify_all and verify_all[0]['motmurlsuffix']: print(f"DEBUG: SUCCESS! URL suffix updated to: {verify_all[0]['motmurlsuffix']}") flash('MotM URL https://motm.ervine.cloud/motm/'+urlSuffix) elif form.activateButton.data: # Generate a fixture number based on the date _nextFixture = _nextMatchDate.replace('-', '') try: sql4 = text(f"ALTER TABLE _hkfc_c_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) except Exception as e: # Columns already exist, which is fine - just log at debug level error_msg = str(e) if 'already exists' in error_msg or 'duplicate' in error_msg.lower(): # This is expected if columns already exist, don't log as error print(f"Columns for fixture {_nextFixture} already exist (this is normal)") else: # Unexpected error, log it print(f"Error adding columns for fixture {_nextFixture}: {e}") # Get or generate the URL suffix print("DEBUG: Getting URL suffix from database") sql5 = text("SELECT motmurlsuffix FROM motmadminsettings") tempSuffix = sql_read_static(sql5) print(f"DEBUG: Query result: {tempSuffix}") if not tempSuffix or not tempSuffix[0]['motmurlsuffix']: # Generate a new URL suffix if one doesn't exist print("DEBUG: URL suffix is None or empty, generating new one") urlSuffix = randomUrlSuffix(8) print(f"DEBUG: Generated new suffix: {urlSuffix}") sql6 = text("UPDATE motmadminsettings SET motmurlsuffix = :url_suffix") result = sql_write_static(sql6, {'url_suffix': urlSuffix}) print(f"DEBUG: UPDATE result: {result}") currSuffix = urlSuffix flash('New voting URL generated') else: currSuffix = tempSuffix[0]['motmurlsuffix'] print(f"DEBUG: Using existing suffix: {currSuffix}") print(f"DEBUG: Final suffix: {currSuffix}") flash('Man of the Match vote is now activated') flash('MotM URL https://motm.ervine.cloud/motm/'+currSuffix) else: flash('Something went wrong - check with Smithers') # Load current settings to populate the form sql_current = text("SELECT nextdate, nextteam, currmotm, currdotd, votingdeadline FROM motmadminsettings") current_settings = sql_read_static(sql_current) if current_settings: from datetime import datetime try: current_date = current_settings[0]['nextdate'] if isinstance(current_date, str): current_date = datetime.strptime(current_date, '%Y-%m-%d').date() form.nextMatchDate.data = current_date except: pass # Pre-populate the next team field if current_settings[0].get('nextteam'): form.nextOppoTeam.data = current_settings[0]['nextteam'] # Pre-populate the voting deadline field if current_settings[0].get('votingdeadline'): deadline = current_settings[0]['votingdeadline'] if isinstance(deadline, str): try: deadline = datetime.strptime(deadline, '%Y-%m-%d %H:%M:%S') except ValueError: pass if hasattr(deadline, 'strftime'): form.votingDeadline.data = deadline.strftime('%Y-%m-%dT%H:%M') # Get automatically determined previous winners auto_prev_motm, auto_prev_dotd = get_previous_match_winners() sql4 = text("SELECT hockeyclub FROM menshockeyclubs ORDER BY hockeyclub") sql5 = text("SELECT nextclub, oppologo FROM motmadminsettings") # Get all players for the dropdown sql6 = text("SELECT playernumber, playerforenames, playersurname, playernickname FROM _hkfc_players ORDER BY playernickname") clubs = sql_read_static(sql4) settings = sql_read_static(sql5) players = sql_read(sql6) # Handle empty results gracefully if not clubs: clubs = [] if not settings: settings = [{'nextclub': 'Unknown', 'oppologo': '/static/images/default_logo.png'}] if not players: players = [] form.nextOppoClub.choices = [(oppo['hockeyclub'], oppo['hockeyclub']) for oppo in clubs] # Build player choices with nickname for better identification form.currMotM.choices = [('0', '-- No Previous MOTM --')] + [(str(player['playernumber']), f"{player['playernickname']} ({player['playerforenames']} {player['playersurname']})") for player in players] form.currDotD.choices = [('0', '-- No Previous DotD --')] + [(str(player['playernumber']), f"{player['playernickname']} ({player['playerforenames']} {player['playersurname']})") for player in players] # Pre-select values: use database value if exists, otherwise auto-determine from previous fixture if current_settings: # If database has a value set, use it; otherwise use auto-determined winner if current_settings[0].get('currmotm'): form.currMotM.data = str(current_settings[0]['currmotm']) elif auto_prev_motm: form.currMotM.data = str(auto_prev_motm) else: form.currMotM.data = '0' if current_settings[0].get('currdotd'): form.currDotD.data = str(current_settings[0]['currdotd']) elif auto_prev_dotd: form.currDotD.data = str(auto_prev_dotd) else: form.currDotD.data = '0' else: # No settings in database, use auto-determined or default to '0' form.currMotM.data = str(auto_prev_motm) if auto_prev_motm else '0' form.currDotD.data = str(auto_prev_dotd) if auto_prev_dotd else '0' # Get the opposition logo using S3 service clubLogo = s3_asset_service.get_asset_url('images/default_logo.png') # Default fallback if settings and settings[0]['nextclub']: nextClub = settings[0]['nextclub'] # Get the club logo from the clubs table sql_club_logo = text("SELECT logo_url FROM clubs WHERE hockey_club = :club_name") club_logo_result = sql_read(sql_club_logo, {'club_name': nextClub}) if club_logo_result and club_logo_result[0]['logo_url']: clubLogo = s3_asset_service.get_logo_url(club_logo_result[0]['logo_url'], nextClub) return render_template('motm_admin.html', form=form, nextOppoLogo=clubLogo) @app.route('/admin/players', methods=['GET']) @basic_auth.required def player_management(): """Admin page for managing players""" sql = text("SELECT playernumber, playerforenames, playersurname, playernickname, playerteam FROM _hkfc_players ORDER BY playernumber") players = sql_read(sql) return render_template('player_management.html', players=players) @app.route('/admin/players/add', methods=['GET', 'POST']) @basic_auth.required def add_player(): """Add a new player""" form = PlayerForm() if form.validate_on_submit(): if form.save_player.data: # Check if player number already exists sql_check = text("SELECT playernumber FROM _hkfc_players WHERE playernumber = :player_number") existing = sql_read(sql_check, {'player_number': form.player_number.data}) if existing: flash('Player number already exists!', 'error') return render_template('add_player.html', form=form) # Insert new player sql = text("INSERT INTO _hkfc_players (playernumber, playerforenames, playersurname, playernickname, playerteam) VALUES (:player_number, :forenames, :surname, :nickname, :team)") sql_write(sql, { 'player_number': form.player_number.data, 'forenames': form.player_forenames.data, 'surname': form.player_surname.data, 'nickname': form.player_nickname.data, 'team': form.player_team.data }) flash('Player added successfully!', 'success') return redirect(url_for('player_management')) elif form.cancel.data: return redirect(url_for('player_management')) return render_template('add_player.html', form=form) @app.route('/admin/players/edit/', methods=['GET', 'POST']) @basic_auth.required def edit_player(player_number): """Edit an existing player""" form = PlayerForm() if request.method == 'GET': # Load player data sql = text("SELECT playernumber, playerforenames, playersurname, playernickname, playerteam FROM _hkfc_players WHERE playernumber = :player_number") player_data = sql_read(sql, {'player_number': player_number}) if not player_data: flash('Player not found!', 'error') return redirect(url_for('player_management')) player = player_data[0] form.player_number.data = player['playernumber'] form.player_forenames.data = player['playerforenames'] form.player_surname.data = player['playersurname'] form.player_nickname.data = player['playernickname'] form.player_team.data = player['playerteam'] if form.validate_on_submit(): if form.save_player.data: # Update player sql = text("UPDATE _hkfc_players SET playerforenames = :forenames, playersurname = :surname, playernickname = :nickname, playerteam = :team WHERE playernumber = :player_number") sql_write(sql, { 'forenames': form.player_forenames.data, 'surname': form.player_surname.data, 'nickname': form.player_nickname.data, 'team': form.player_team.data, 'player_number': player_number }) flash('Player updated successfully!', 'success') return redirect(url_for('player_management')) elif form.cancel.data: return redirect(url_for('player_management')) return render_template('edit_player.html', form=form, player_number=player_number) @app.route('/admin/players/delete/', methods=['POST']) @basic_auth.required def delete_player(player_number): """Delete a player""" sql = text("DELETE FROM _hkfc_players WHERE playernumber = :player_number") sql_write(sql, {'player_number': player_number}) flash('Player deleted successfully!', 'success') return redirect(url_for('player_management')) @app.route('/admin/squad', methods=['GET']) @basic_auth.required def match_squad(): """Admin page for managing match squad""" sql = text("SELECT playernumber, playerforenames, playersurname, playernickname, playerteam FROM _hkfc_players ORDER BY playerteam, playernumber") players = sql_read(sql) return render_template('match_squad.html', players=players) # ==================== CLUB MANAGEMENT ==================== @app.route('/admin/clubs', methods=['GET']) @basic_auth.required def club_management(): """Admin page for managing clubs""" sql = text("SELECT id, hockey_club, logo_url FROM clubs ORDER BY hockey_club") clubs = sql_read(sql) return render_template('club_management.html', clubs=clubs, s3_asset_service=s3_asset_service) @app.route('/admin/clubs/add', methods=['GET', 'POST']) @basic_auth.required def add_club(): """Add a new club""" form = ClubForm() if form.validate_on_submit(): if form.save_club.data: # Check if club already exists sql_check = text("SELECT hockey_club FROM clubs WHERE hockey_club = :club_name") existing = sql_read(sql_check, {'club_name': form.hockey_club.data}) if existing: flash('Club already exists!', 'error') return render_template('add_club.html', form=form) # Insert new club sql = text("INSERT INTO clubs (hockey_club, logo_url) VALUES (:club_name, :logo_url)") sql_write(sql, { 'club_name': form.hockey_club.data, 'logo_url': form.logo_url.data }) flash('Club added successfully!', 'success') return redirect(url_for('club_management')) elif form.cancel.data: return redirect(url_for('club_management')) return render_template('add_club.html', form=form) @app.route('/admin/clubs/edit/', methods=['GET', 'POST']) @basic_auth.required def edit_club(club_id): """Edit an existing club""" form = ClubForm() if request.method == 'GET': # Load club data sql = text("SELECT id, hockey_club, logo_url FROM clubs WHERE id = :club_id") club_data = sql_read(sql, {'club_id': club_id}) if not club_data: flash('Club not found!', 'error') return redirect(url_for('club_management')) club = club_data[0] form.hockey_club.data = club['hockey_club'] form.logo_url.data = club['logo_url'] if form.validate_on_submit(): if form.save_club.data: # Update club sql = text("UPDATE clubs SET hockey_club = :club_name, logo_url = :logo_url WHERE id = :club_id") sql_write(sql, { 'club_name': form.hockey_club.data, 'logo_url': form.logo_url.data, 'club_id': club_id }) flash('Club updated successfully!', 'success') return redirect(url_for('club_management')) elif form.cancel.data: return redirect(url_for('club_management')) return render_template('edit_club.html', form=form, club_id=club_id) @app.route('/admin/clubs/delete/', methods=['POST']) @basic_auth.required def delete_club(club_id): """Delete a club""" sql = text("DELETE FROM clubs WHERE id = :club_id") sql_write(sql, {'club_id': club_id}) flash('Club deleted successfully!', 'success') return redirect(url_for('club_management')) # ==================== TEAM MANAGEMENT ==================== @app.route('/admin/teams', methods=['GET']) @basic_auth.required def team_management(): """Admin page for managing teams""" sql = text("SELECT id, club, team, display_name, league FROM teams ORDER BY club, team") teams = sql_read(sql) return render_template('team_management.html', teams=teams) @app.route('/admin/teams/add', methods=['GET', 'POST']) @basic_auth.required def add_team(): """Add a new team""" form = TeamForm() # Populate club choices sql_clubs = text("SELECT hockey_club FROM clubs ORDER BY hockey_club") clubs = sql_read(sql_clubs) form.club.choices = [(club['hockey_club'], club['hockey_club']) for club in clubs] if form.validate_on_submit(): if form.save_team.data: # Check if team already exists sql_check = text("SELECT club, team FROM teams WHERE club = :club AND team = :team") existing = sql_read(sql_check, {'club': form.club.data, 'team': form.team.data}) if existing: flash('Team already exists!', 'error') return render_template('add_team.html', form=form) # Insert new team sql = text("INSERT INTO teams (club, team, display_name, league) VALUES (:club, :team, :display_name, :league)") sql_write(sql, { 'club': form.club.data, 'team': form.team.data, 'display_name': form.display_name.data, 'league': form.league.data }) flash('Team added successfully!', 'success') return redirect(url_for('team_management')) elif form.cancel.data: return redirect(url_for('team_management')) return render_template('add_team.html', form=form) @app.route('/admin/teams/edit/', methods=['GET', 'POST']) @basic_auth.required def edit_team(team_id): """Edit an existing team""" form = TeamForm() # Populate club choices sql_clubs = text("SELECT hockey_club FROM clubs ORDER BY hockey_club") clubs = sql_read(sql_clubs) form.club.choices = [(club['hockey_club'], club['hockey_club']) for club in clubs] if request.method == 'GET': # Load team data sql = text("SELECT id, club, team, display_name, league FROM teams WHERE id = :team_id") team_data = sql_read(sql, {'team_id': team_id}) if not team_data: flash('Team not found!', 'error') return redirect(url_for('team_management')) team = team_data[0] form.club.data = team['club'] form.team.data = team['team'] form.display_name.data = team['display_name'] form.league.data = team['league'] if form.validate_on_submit(): if form.save_team.data: # Update team sql = text("UPDATE teams SET club = :club, team = :team, display_name = :display_name, league = :league WHERE id = :team_id") sql_write(sql, { 'club': form.club.data, 'team': form.team.data, 'display_name': form.display_name.data, 'league': form.league.data, 'team_id': team_id }) flash('Team updated successfully!', 'success') return redirect(url_for('team_management')) elif form.cancel.data: return redirect(url_for('team_management')) return render_template('edit_team.html', form=form, team_id=team_id) @app.route('/admin/teams/delete/', methods=['POST']) @basic_auth.required def delete_team(team_id): """Delete a team""" sql = text("DELETE FROM teams WHERE id = :team_id") sql_write(sql, {'team_id': team_id}) flash('Team deleted successfully!', 'success') return redirect(url_for('team_management')) # ==================== DATA IMPORT ==================== @app.route('/admin/import', methods=['GET', 'POST']) @basic_auth.required def data_import(): """Import data from Hong Kong Hockey Association""" form = DataImportForm() if form.validate_on_submit(): if form.import_data.data: imported_clubs = 0 imported_teams = 0 imported_players = 0 if form.import_clubs.data: # Import clubs based on Hong Kong Hockey Association data clubs_data = [ {'hockey_club': 'HKFC', 'logo_url': '/static/images/hkfclogo.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'}, {'hockey_club': 'SSSC', 'logo_url': '/static/images/sssc_logo.png'}, {'hockey_club': 'Dragons', 'logo_url': '/static/images/dragons_logo.png'}, {'hockey_club': 'Kai Tak', 'logo_url': '/static/images/kaitak_logo.png'}, {'hockey_club': 'RHOBA', 'logo_url': '/static/images/rhoba_logo.png'}, {'hockey_club': 'Elite', 'logo_url': '/static/images/elite_logo.png'}, {'hockey_club': 'Aquila', 'logo_url': '/static/images/aquila_logo.png'}, {'hockey_club': 'HKJ', 'logo_url': '/static/images/hkj_logo.png'}, {'hockey_club': 'Sirius', 'logo_url': '/static/images/sirius_logo.png'}, {'hockey_club': 'Shaheen', 'logo_url': '/static/images/shaheen_logo.png'}, {'hockey_club': 'Diocesan', 'logo_url': '/static/images/diocesan_logo.png'}, {'hockey_club': 'Rhino', 'logo_url': '/static/images/rhino_logo.png'}, {'hockey_club': 'Khalsa', 'logo_url': '/static/images/khalsa_logo.png'}, {'hockey_club': 'HKCC', 'logo_url': '/static/images/hkcc_logo.png'}, {'hockey_club': 'Police', 'logo_url': '/static/images/police_logo.png'}, {'hockey_club': 'Recreio', 'logo_url': '/static/images/recreio_logo.png'}, {'hockey_club': 'CSD', 'logo_url': '/static/images/csd_logo.png'}, {'hockey_club': 'Dutch', 'logo_url': '/static/images/dutch_logo.png'}, {'hockey_club': 'HKUHC', 'logo_url': '/static/images/hkuhc_logo.png'}, {'hockey_club': 'Kaitiaki', 'logo_url': '/static/images/kaitiaki_logo.png'}, {'hockey_club': 'Antlers', 'logo_url': '/static/images/antlers_logo.png'}, {'hockey_club': 'Marcellin', 'logo_url': '/static/images/marcellin_logo.png'}, {'hockey_club': 'Skyers', 'logo_url': '/static/images/skyers_logo.png'}, {'hockey_club': 'JR', 'logo_url': '/static/images/jr_logo.png'}, {'hockey_club': 'IUHK', 'logo_url': '/static/images/iuhk_logo.png'}, {'hockey_club': '144U', 'logo_url': '/static/images/144u_logo.png'}, {'hockey_club': 'HKU', 'logo_url': '/static/images/hku_logo.png'}, ] for club_data in clubs_data: # Check if club already exists sql_check = text("SELECT hockey_club FROM clubs WHERE hockey_club = :club_name") existing = sql_read(sql_check, {'club_name': club_data['hockey_club']}) if not existing: sql = text("INSERT INTO clubs (hockey_club, logo_url) VALUES (:club_name, :logo_url)") sql_write(sql, club_data) imported_clubs += 1 if form.import_teams.data: # Import teams based on Hong Kong Hockey Association divisions teams_data = [ # Premier Division {'club': 'HKFC', 'team': 'A', 'display_name': 'HKFC A', 'league': 'Premier Division'}, {'club': 'KCC', 'team': 'A', 'display_name': 'KCC A', 'league': 'Premier Division'}, {'club': 'USRC', 'team': 'A', 'display_name': 'USRC A', 'league': 'Premier Division'}, {'club': 'Valley', 'team': 'A', 'display_name': 'Valley A', 'league': 'Premier Division'}, # 1st Division {'club': 'HKFC', 'team': 'B', 'display_name': 'HKFC B', 'league': '1st Division'}, {'club': 'KCC', 'team': 'B', 'display_name': 'KCC B', 'league': '1st Division'}, {'club': 'USRC', 'team': 'B', 'display_name': 'USRC B', 'league': '1st Division'}, {'club': 'Valley', 'team': 'B', 'display_name': 'Valley B', 'league': '1st Division'}, # 2nd Division {'club': 'HKFC', 'team': 'C', 'display_name': 'HKFC C', 'league': '2nd Division'}, {'club': 'KCC', 'team': 'C', 'display_name': 'KCC C', 'league': '2nd Division'}, {'club': 'USRC', 'team': 'C', 'display_name': 'USRC C', 'league': '2nd Division'}, {'club': 'Valley', 'team': 'C', 'display_name': 'Valley C', 'league': '2nd Division'}, # 3rd Division {'club': 'SSSC', 'team': 'C', 'display_name': 'SSSC C', 'league': '3rd Division'}, {'club': 'Dragons', 'team': 'A', 'display_name': 'Dragons A', 'league': '3rd Division'}, {'club': 'Kai Tak', 'team': 'B', 'display_name': 'Kai Tak B', 'league': '3rd Division'}, {'club': 'RHOBA', 'team': 'A', 'display_name': 'RHOBA A', 'league': '3rd Division'}, {'club': 'Elite', 'team': 'B', 'display_name': 'Elite B', 'league': '3rd Division'}, {'club': 'HKFC', 'team': 'F', 'display_name': 'HKFC F', 'league': '3rd Division'}, {'club': 'Aquila', 'team': 'A', 'display_name': 'Aquila A', 'league': '3rd Division'}, {'club': 'HKJ', 'team': 'B', 'display_name': 'HKJ B', 'league': '3rd Division'}, {'club': 'Sirius', 'team': 'A', 'display_name': 'Sirius A', 'league': '3rd Division'}, {'club': 'Shaheen', 'team': 'B', 'display_name': 'Shaheen B', 'league': '3rd Division'}, {'club': 'RHOBA', 'team': 'B', 'display_name': 'RHOBA B', 'league': '3rd Division'}, # 4th Division {'club': 'Khalsa', 'team': 'C', 'display_name': 'Khalsa C', 'league': '4th Division'}, {'club': 'HKCC', 'team': 'C', 'display_name': 'HKCC C', 'league': '4th Division'}, {'club': 'Valley', 'team': 'D', 'display_name': 'Valley D', 'league': '4th Division'}, {'club': 'Police', 'team': 'A', 'display_name': 'Police A', 'league': '4th Division'}, {'club': 'Recreio', 'team': 'A', 'display_name': 'Recreio A', 'league': '4th Division'}, {'club': 'CSD', 'team': 'A', 'display_name': 'CSD A', 'league': '4th Division'}, {'club': 'HKFC', 'team': 'G', 'display_name': 'HKFC G', 'league': '4th Division'}, {'club': 'Dutch', 'team': 'B', 'display_name': 'Dutch B', 'league': '4th Division'}, {'club': 'RHOBA', 'team': 'C', 'display_name': 'RHOBA C', 'league': '4th Division'}, {'club': 'HKUHC', 'team': 'A', 'display_name': 'HKUHC A', 'league': '4th Division'}, {'club': 'Kaitiaki', 'team': 'A', 'display_name': 'Kaitiaki', 'league': '4th Division'}, # 5th Division {'club': 'KCC', 'team': 'D', 'display_name': 'KCC D', 'league': '5th Division'}, {'club': 'Kai Tak', 'team': 'C', 'display_name': 'Kai Tak C', 'league': '5th Division'}, {'club': 'Dragons', 'team': 'B', 'display_name': 'Dragons B', 'league': '5th Division'}, {'club': 'Antlers', 'team': 'C', 'display_name': 'Antlers C', 'league': '5th Division'}, {'club': 'Valley', 'team': 'E', 'display_name': 'Valley E', 'league': '5th Division'}, {'club': 'Elite', 'team': 'C', 'display_name': 'Elite C', 'league': '5th Division'}, {'club': 'HKFC', 'team': 'H', 'display_name': 'HKFC H', 'league': '5th Division'}, {'club': 'Aquila', 'team': 'B', 'display_name': 'Aquila B', 'league': '5th Division'}, {'club': 'Sirius', 'team': 'B', 'display_name': 'Sirius B', 'league': '5th Division'}, {'club': 'Marcellin', 'team': 'A', 'display_name': 'Marcellin A', 'league': '5th Division'}, {'club': 'Recreio', 'team': 'B', 'display_name': 'Recreio B', 'league': '5th Division'}, {'club': 'Diocesan', 'team': 'B', 'display_name': 'Diocesan B', 'league': '5th Division'}, # 6th Division {'club': 'Rhino', 'team': 'B', 'display_name': 'Rhino B', 'league': '6th Division'}, {'club': 'Skyers', 'team': 'A', 'display_name': 'Skyers A', 'league': '6th Division'}, {'club': 'JR', 'team': 'A', 'display_name': 'JR', 'league': '6th Division'}, {'club': 'HKCC', 'team': 'D', 'display_name': 'HKCC D', 'league': '6th Division'}, {'club': 'KCC', 'team': 'E', 'display_name': 'KCC E', 'league': '6th Division'}, {'club': 'HKJ', 'team': 'C', 'display_name': 'HKJ C', 'league': '6th Division'}, {'club': 'IUHK', 'team': 'A', 'display_name': 'IUHK A', 'league': '6th Division'}, {'club': 'Valley', 'team': 'F', 'display_name': 'Valley F', 'league': '6th Division'}, {'club': '144U', 'team': 'A', 'display_name': '144U A', 'league': '6th Division'}, {'club': 'HKU', 'team': 'A', 'display_name': 'HKU A', 'league': '6th Division'}, ] for team_data in teams_data: # Check if team already exists sql_check = text("SELECT club, team FROM teams WHERE club = :club AND team = :team") existing = sql_read(sql_check, {'club': team_data['club'], 'team': team_data['team']}) if not existing: sql = text("INSERT INTO teams (club, team, display_name, league) VALUES (:club, :team, :display_name, :league)") sql_write(sql, team_data) imported_teams += 1 if form.import_players.data: # Import sample players for HKFC C team 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 sql_check = text("SELECT playernumber FROM _hkfc_players WHERE playernumber = :player_number") existing = sql_read(sql_check, {'player_number': player_data['player_number']}) if not existing: sql = text("INSERT INTO _hkfc_players (playernumber, playerforenames, playersurname, playernickname, playerteam) VALUES (:player_number, :forenames, :surname, :nickname, :team)") sql_write(sql, player_data) imported_players += 1 flash(f'Import completed! {imported_clubs} clubs, {imported_teams} teams, {imported_players} players imported.', 'success') return redirect(url_for('data_import')) elif form.cancel.data: return redirect(url_for('data_import')) return render_template('data_import.html', form=form) @app.route('/admin/import/clubs/select', methods=['GET', 'POST']) @basic_auth.required def club_selection(): """Club selection page for importing specific clubs""" form = ClubSelectionForm() # Fetch clubs from the website try: clubs = get_hk_hockey_clubs() if not clubs: # Fallback to predefined clubs if scraping fails clubs = [ {'name': 'Hong Kong Football Club', 'abbreviation': 'HKFC', 'teams': ['A', 'B', 'C', 'F', 'G', 'H'], 'convenor': None, 'email': None}, {'name': 'Kowloon Cricket Club', 'abbreviation': 'KCC', 'teams': ['A', 'B', 'C', 'D', 'E'], 'convenor': None, 'email': None}, {'name': 'United Services Recreation Club', 'abbreviation': 'USRC', 'teams': ['A', 'B', 'C'], 'convenor': None, 'email': None}, {'name': 'Valley Fort Sports Club', 'abbreviation': 'Valley', 'teams': ['A', 'B', 'C', 'D', 'E', 'F'], 'convenor': None, 'email': None}, {'name': 'South China Sports Club', 'abbreviation': 'SSSC', 'teams': ['C'], 'convenor': None, 'email': None}, {'name': 'Dragons Hockey Club', 'abbreviation': 'Dragons', 'teams': ['A', 'B'], 'convenor': None, 'email': None}, {'name': 'Kai Tak Sports Club', 'abbreviation': 'Kai Tak', 'teams': ['B', 'C'], 'convenor': None, 'email': None}, {'name': 'Royal Hong Kong Regiment Officers and Businessmen Association', 'abbreviation': 'RHOBA', 'teams': ['A', 'B', 'C'], 'convenor': None, 'email': None}, {'name': 'Elite Hockey Club', 'abbreviation': 'Elite', 'teams': ['B', 'C'], 'convenor': None, 'email': None}, {'name': 'Aquila Hockey Club', 'abbreviation': 'Aquila', 'teams': ['A', 'B'], 'convenor': None, 'email': None}, {'name': 'Hong Kong Jockey Club', 'abbreviation': 'HKJ', 'teams': ['B', 'C'], 'convenor': None, 'email': None}, {'name': 'Sirius Hockey Club', 'abbreviation': 'Sirius', 'teams': ['A', 'B'], 'convenor': None, 'email': None}, {'name': 'Shaheen Hockey Club', 'abbreviation': 'Shaheen', 'teams': ['B'], 'convenor': None, 'email': None}, {'name': 'Diocesan Boys School', 'abbreviation': 'Diocesan', 'teams': ['B'], 'convenor': None, 'email': None}, {'name': 'Rhino Hockey Club', 'abbreviation': 'Rhino', 'teams': ['B'], 'convenor': None, 'email': None}, {'name': 'Khalsa Hockey Club', 'abbreviation': 'Khalsa', 'teams': ['C'], 'convenor': None, 'email': None}, {'name': 'Hong Kong Cricket Club', 'abbreviation': 'HKCC', 'teams': ['C', 'D'], 'convenor': None, 'email': None}, {'name': 'Hong Kong Police Force', 'abbreviation': 'Police', 'teams': ['A'], 'convenor': None, 'email': None}, {'name': 'Recreio Hockey Club', 'abbreviation': 'Recreio', 'teams': ['A', 'B'], 'convenor': None, 'email': None}, {'name': 'Correctional Services Department', 'abbreviation': 'CSD', 'teams': ['A'], 'convenor': None, 'email': None}, {'name': 'Dutch Hockey Club', 'abbreviation': 'Dutch', 'teams': ['B'], 'convenor': None, 'email': None}, {'name': 'Hong Kong University Hockey Club', 'abbreviation': 'HKUHC', 'teams': ['A'], 'convenor': None, 'email': None}, {'name': 'Kaitiaki Hockey Club', 'abbreviation': 'Kaitiaki', 'teams': ['A'], 'convenor': None, 'email': None}, {'name': 'Antlers Hockey Club', 'abbreviation': 'Antlers', 'teams': ['C'], 'convenor': None, 'email': None}, {'name': 'Marcellin Hockey Club', 'abbreviation': 'Marcellin', 'teams': ['A'], 'convenor': None, 'email': None}, {'name': 'Skyers Hockey Club', 'abbreviation': 'Skyers', 'teams': ['A'], 'convenor': None, 'email': None}, {'name': 'JR Hockey Club', 'abbreviation': 'JR', 'teams': ['A'], 'convenor': None, 'email': None}, {'name': 'International University of Hong Kong', 'abbreviation': 'IUHK', 'teams': ['A'], 'convenor': None, 'email': None}, {'name': '144 United Hockey Club', 'abbreviation': '144U', 'teams': ['A'], 'convenor': None, 'email': None}, {'name': 'Hong Kong University', 'abbreviation': 'HKU', 'teams': ['A'], 'convenor': None, 'email': None}, ] except Exception as e: flash(f'Error fetching clubs: {str(e)}', 'error') clubs = [] if request.method == 'POST': if form.select_all.data: # Select all clubs selected_clubs = [club['name'] for club in clubs] return render_template('club_selection.html', form=form, clubs=clubs, selected_clubs=selected_clubs) elif form.select_none.data: # Select no clubs return render_template('club_selection.html', form=form, clubs=clubs, selected_clubs=[]) elif form.import_selected.data: # Import selected clubs selected_clubs = request.form.getlist('selected_clubs') if not selected_clubs: flash('No clubs selected for import!', 'error') return render_template('club_selection.html', form=form, clubs=clubs, selected_clubs=[]) imported_count = 0 skipped_count = 0 for club_name in selected_clubs: # Find the club data club_data = next((club for club in clubs if club['name'] == club_name), None) if not club_data: continue # Check if club already exists sql_check = text("SELECT hockey_club FROM clubs WHERE hockey_club = :club_name") existing = sql_read(sql_check, {'club_name': club_name}) if existing: skipped_count += 1 continue # Import the club logo_url = f"/static/images/clubs/{club_name.lower().replace(' ', '_').replace('.', '').replace(',', '')}_logo.png" sql = text("INSERT INTO clubs (hockey_club, logo_url) VALUES (:club_name, :logo_url)") sql_write(sql, {'club_name': club_name, 'logo_url': logo_url}) imported_count += 1 if imported_count > 0: flash(f'Successfully imported {imported_count} clubs!', 'success') if skipped_count > 0: flash(f'Skipped {skipped_count} clubs that already exist.', 'info') return redirect(url_for('club_management')) elif form.cancel.data: return redirect(url_for('data_import')) return render_template('club_selection.html', form=form, clubs=clubs, selected_clubs=[]) @app.route('/admin/device-tracking', methods=['GET', 'POST']) @basic_auth.required def device_tracking(): """Admin page for viewing device voting patterns""" if request.method == 'POST': action = request.form.get('action') if action == 'analyze_patterns': # Analyze voting patterns by device sql_patterns = text(""" SELECT device_id, COUNT(*) as vote_count, COUNT(DISTINCT fixture_date) as fixtures_voted, STRING_AGG(DISTINCT motm_player_name, ', ') as motm_players, STRING_AGG(DISTINCT dotd_player_name, ', ') as dotd_players, MIN(vote_timestamp) as first_vote, MAX(vote_timestamp) as last_vote, STRING_AGG(DISTINCT ip_address::text, ', ') as ip_addresses FROM device_votes GROUP BY device_id HAVING COUNT(*) > 1 ORDER BY vote_count DESC, fixtures_voted DESC """) patterns = sql_read(sql_patterns) return render_template('device_tracking.html', patterns=patterns, analysis_mode=True) elif action == 'view_device_details': device_id = request.form.get('device_id') if device_id: sql_details = text(""" SELECT device_id, fixture_date, motm_player_name, dotd_player_name, ip_address, user_agent, vote_timestamp FROM device_votes WHERE device_id = :device_id ORDER BY vote_timestamp DESC """) device_details = sql_read(sql_details, {'device_id': device_id}) return render_template('device_tracking.html', device_details=device_details, selected_device=device_id, details_mode=True) # Default view - show recent votes sql_recent = text(""" SELECT device_id, fixture_date, motm_player_name, dotd_player_name, ip_address, vote_timestamp FROM device_votes ORDER BY vote_timestamp DESC LIMIT 50 """) recent_votes = sql_read(sql_recent) return render_template('device_tracking.html', recent_votes=recent_votes) @app.route('/admin/motm/manage', methods=['GET', 'POST']) @basic_auth.required def motm_management(): """Manage MOTM/DotD counts and reset functionality""" if request.method == 'POST': action = request.form.get('action') if action == 'reset_fixture': fixture_date = request.form.get('fixture_date') if fixture_date: motm_col = f'motm_{fixture_date}' dotd_col = f'dotd_{fixture_date}' # Reset fixture-specific columns sql_reset = text(f""" UPDATE _hkfc_c_motm SET {motm_col} = 0, {dotd_col} = 0 WHERE {motm_col} > 0 OR {dotd_col} > 0 """) sql_write(sql_reset) flash(f'Reset MOTM/DotD counts for fixture {fixture_date}', 'success') elif action == 'reset_totals': # Reset all total columns sql_reset_totals = text(""" UPDATE _hkfc_c_motm SET motmtotal = 0, dotdtotal = 0, assiststotal = 0, goalstotal = 0 """) sql_write(sql_reset_totals) flash('Reset all MOTM/DotD totals', 'success') elif action == 'reset_all': # Reset everything - dynamically reset all fixture columns sql_columns = text(""" SELECT column_name FROM information_schema.columns WHERE table_name = '_hkfc_c_motm' AND (column_name LIKE 'motm_%' OR column_name LIKE 'dotd_%') AND column_name NOT LIKE '%total' """) columns = sql_read(sql_columns) # Build dynamic SET clause set_clauses = ['motmtotal = 0', 'dotdtotal = 0', 'assiststotal = 0', 'goalstotal = 0'] for col in columns: set_clauses.append(f"{col['column_name']} = 0") sql_reset_all = text(f""" UPDATE _hkfc_c_motm SET {', '.join(set_clauses)} """) sql_write(sql_reset_all) flash('Reset all MOTM/DotD data', 'success') elif action == 'reset_player_fixture': player_number = request.form.get('player_number') fixture_date = request.form.get('fixture_date') if player_number and fixture_date: motm_col = f'motm_{fixture_date}' dotd_col = f'dotd_{fixture_date}' # Reset specific player's fixture counts sql_reset_player = text(f""" UPDATE _hkfc_c_motm SET {motm_col} = 0, {dotd_col} = 0 WHERE playernumber = :player_number """) sql_write(sql_reset_player, {'player_number': player_number}) flash(f'Reset MOTM/DotD counts for player #{player_number} in fixture {fixture_date}', 'success') elif action == 'drop_column': column_name = request.form.get('column_name') if column_name: try: sql_drop = text(f"ALTER TABLE _hkfc_c_motm DROP COLUMN {column_name}") sql_write(sql_drop) flash(f'Successfully dropped column {column_name}', 'success') except Exception as e: flash(f'Error dropping column {column_name}: {str(e)}', 'error') elif action == 'drop_fixture_columns': fixture_date = request.form.get('fixture_date') if fixture_date: motm_col = f'motm_{fixture_date}' dotd_col = f'dotd_{fixture_date}' try: sql_drop_motm = text(f"ALTER TABLE _hkfc_c_motm DROP COLUMN {motm_col}") sql_drop_dotd = text(f"ALTER TABLE _hkfc_c_motm DROP COLUMN {dotd_col}") sql_write(sql_drop_motm) sql_write(sql_drop_dotd) flash(f'Successfully dropped columns for fixture {fixture_date}', 'success') except Exception as e: flash(f'Error dropping fixture columns: {str(e)}', 'error') elif action == 'sync_totals': # Sync stored totals with calculated totals sql_data = text("SELECT * FROM _hkfc_c_motm") players = sql_read(sql_data) updated_count = 0 for player in players: motm_total = 0 dotd_total = 0 # Calculate totals from fixture columns for col_name in player.keys(): if col_name.startswith('motm_') and col_name != 'motmtotal': motm_total += player[col_name] or 0 elif col_name.startswith('dotd_') and col_name != 'dotdtotal': dotd_total += player[col_name] or 0 # Update stored totals if they differ if player.get('motmtotal', 0) != motm_total or player.get('dotdtotal', 0) != dotd_total: sql_update = text("UPDATE _hkfc_c_motm SET motmtotal = :motm_total, dotdtotal = :dotd_total WHERE playernumber = :player_num") sql_write(sql_update, { 'motm_total': motm_total, 'dotd_total': dotd_total, 'player_num': player['playernumber'] }) updated_count += 1 flash(f'Synced totals for {updated_count} players', 'success') # Get all fixture columns sql_columns = text(""" SELECT column_name FROM information_schema.columns WHERE table_name = '_hkfc_c_motm' AND column_name LIKE 'motm_%' OR column_name LIKE 'dotd_%' ORDER BY column_name """) columns = sql_read(sql_columns) # Extract unique fixture dates fixture_dates = set() for col in columns: if col['column_name'].startswith('motm_') or col['column_name'].startswith('dotd_'): date_part = col['column_name'].split('_')[1] if date_part != 'total': fixture_dates.add(date_part) fixture_dates = sorted(fixture_dates, reverse=True) # Get current data with dynamic totals sql_data = text("SELECT * FROM _hkfc_c_motm ORDER BY playernumber") motm_data = sql_read(sql_data) # Calculate dynamic totals for each player for player in motm_data: motm_total = 0 dotd_total = 0 # Sum all fixture-specific columns for col_name in player.keys(): if col_name.startswith('motm_') and col_name != 'motmtotal': motm_total += player[col_name] or 0 elif col_name.startswith('dotd_') and col_name != 'dotdtotal': dotd_total += player[col_name] or 0 player['calculated_motmtotal'] = motm_total player['calculated_dotdtotal'] = dotd_total return render_template('motm_management.html', fixture_dates=fixture_dates, motm_data=motm_data) @app.route('/admin/comments/manage', methods=['GET', 'POST']) @basic_auth.required def comments_management(): """Manage match comments with edit and delete functionality""" if request.method == 'POST': action = request.form.get('action') if action == 'delete_comment': comment_id = request.form.get('comment_id') match_date = request.form.get('match_date') original_comment = request.form.get('original_comment') if comment_id and match_date and original_comment: try: # For PostgreSQL, use ctid if available, otherwise match on date and comment try: # Try using ctid (PostgreSQL) sql_delete = text("DELETE FROM _motmcomments WHERE ctid = :comment_id::tid") sql_write(sql_delete, {'comment_id': comment_id}) flash('Comment deleted successfully', 'success') except: # Fallback: delete by matching matchDate and comment # PostgreSQL doesn't support LIMIT in DELETE without using ctid sql_delete = text(""" DELETE FROM _motmcomments WHERE ctid IN ( SELECT ctid FROM _motmcomments WHERE matchDate = :match_date AND comment = :comment LIMIT 1 ) """) sql_write(sql_delete, {'match_date': match_date, 'comment': original_comment}) flash('Comment deleted successfully', 'success') except Exception as e: flash(f'Error deleting comment: {str(e)}', 'error') elif action == 'edit_comment': comment_id = request.form.get('comment_id') new_comment = request.form.get('comment') match_date = request.form.get('match_date') original_comment = request.form.get('original_comment') if new_comment and match_date and original_comment: try: # Don't escape single quotes - parameterized queries handle that try: # Try using ctid (PostgreSQL) sql_update = text("UPDATE _motmcomments SET comment = :comment WHERE ctid = :comment_id::tid") sql_write(sql_update, {'comment': new_comment, 'comment_id': comment_id}) flash('Comment updated successfully', 'success') except: # Fallback: update by matching matchDate and original comment # Update only the first matching row using ctid sql_update = text(""" UPDATE _motmcomments SET comment = :new_comment WHERE ctid IN ( SELECT ctid FROM _motmcomments WHERE matchDate = :match_date AND comment = :old_comment LIMIT 1 ) """) sql_write(sql_update, {'new_comment': new_comment, 'match_date': match_date, 'old_comment': original_comment}) flash('Comment updated successfully', 'success') except Exception as e: flash(f'Error updating comment: {str(e)}', 'error') elif action == 'delete_match_comments': match_date = request.form.get('match_date') if match_date: try: sql_delete = text("DELETE FROM _motmcomments WHERE matchDate = :match_date") sql_write(sql_delete, {'match_date': match_date}) flash(f'Deleted all comments for match {match_date}', 'success') except Exception as e: flash(f'Error deleting match comments: {str(e)}', 'error') elif action == 'delete_all_comments': try: sql_delete = text("DELETE FROM _motmcomments") sql_write(sql_delete) flash('All comments deleted successfully', 'success') except Exception as e: flash(f'Error deleting all comments: {str(e)}', 'error') elif action == 'drop_column': column_name = request.form.get('column_name') if column_name: try: sql_drop = text(f"ALTER TABLE _motmcomments DROP COLUMN {column_name}") sql_write(sql_drop) flash(f'Successfully dropped column {column_name}', 'success') except Exception as e: flash(f'Error dropping column: {str(e)}', 'error') # Get all columns from the table try: sql_columns = text(""" SELECT column_name FROM information_schema.columns WHERE table_name = '_motmcomments' ORDER BY ordinal_position """) columns_result = sql_read(sql_columns) table_columns = [col['column_name'] for col in columns_result] if columns_result else ['matchDate', 'comment'] except: # Fallback for SQLite or if information_schema is not available table_columns = ['matchDate', 'comment'] # Get all comments with row IDs # Try different approaches based on database type comments = [] try: # First, try to get the actual table structure to find a primary key sql_pk = text(""" SELECT column_name FROM information_schema.columns WHERE table_name = '_motmcomments' AND column_name IN ('id', 'rowid', 'oid') LIMIT 1 """) pk_result = sql_read(sql_pk) if pk_result: # Use the found primary key column pk_column = pk_result[0]['column_name'] sql_comments = text(f"SELECT {pk_column} as comment_id, matchDate, comment FROM _motmcomments ORDER BY matchDate DESC") comments = sql_read(sql_comments) else: # No explicit ID column, use PostgreSQL's ctid or generate row numbers try: # PostgreSQL: Use ctid (physical row identifier) sql_comments = text("SELECT ctid::text as comment_id, matchDate, comment FROM _motmcomments ORDER BY matchDate DESC") comments = sql_read(sql_comments) except: # If that fails, use ROW_NUMBER() sql_comments = text(""" SELECT ROW_NUMBER() OVER (ORDER BY matchDate DESC) as comment_id, matchDate, comment FROM _motmcomments ORDER BY matchDate DESC """) comments = sql_read(sql_comments) except Exception as e: print(f"Error fetching comments: {e}") # Last resort: Get comments without IDs and generate them in Python try: sql_comments = text("SELECT matchDate, comment FROM _motmcomments ORDER BY matchDate DESC") raw_comments = sql_read(sql_comments) if raw_comments: # Add sequential IDs comments = [ {'comment_id': idx + 1, 'matchDate': c['matchDate'], 'comment': c['comment']} for idx, c in enumerate(raw_comments) ] except: comments = [] # Get unique match dates match_dates = [] if comments: unique_dates = set() for comment in comments: if comment.get('matchDate'): unique_dates.add(str(comment['matchDate'])) match_dates = sorted(unique_dates, reverse=True) return render_template('comments_management.html', comments=comments, match_dates=match_dates, table_columns=table_columns) @app.route('/admin/squad/submit', methods=['POST']) @basic_auth.required def match_squad_submit(): """Process squad selection""" _playerNumbers = request.form.getlist('playerNumber') if not _playerNumbers: flash('No players selected!', 'error') return redirect(url_for('match_squad')) for _playerNumber in _playerNumbers: sql = text("INSERT INTO _hkfcc_matchsquad (playernumber, playerforenames, playersurname, playernickname) SELECT playernumber, playerforenames, playersurname, playernickname FROM _hkfc_players WHERE playernumber = :player_number") sql_write(sql, {'player_number': _playerNumber}) flash(f'Successfully added {len(_playerNumbers)} player(s) to the squad!', 'success') sql2 = text("SELECT playernumber, playerforenames, playersurname, playernickname FROM _hkfcc_matchsquad") players = sql_read(sql2) table = matchSquadTable(players) table.border = True table.classes = ['table-striped', 'table-condensed', 'table-hover'] return render_template('match_squad_selected.html', table=table) @app.route('/admin/squad/list') @basic_auth.required def match_squad_list(): """Display current squad list""" sql = text("SELECT playernumber, playerforenames, playersurname, playernickname FROM _hkfcc_matchsquad") players = sql_read(sql) table = matchSquadTable(players) table.border = True table.classes = ['table-striped', 'table-condensed', 'table-hover'] return render_template('match_squad_selected.html', table=table) @app.route('/admin/squad/history') @basic_auth.required def squad_history(): """View historical squad data""" try: # Get all historical squads grouped by fixture sql = text(""" SELECT fixture_number, match_date, COUNT(*) as player_count, STRING_AGG(player_nickname, ', ') as players FROM squad_history GROUP BY fixture_number, match_date ORDER BY match_date DESC """) history = sql_read(sql) # Get detailed squad for each fixture sql_details = text(""" SELECT fixture_number, match_date, player_number, player_forenames, player_surname, player_nickname, archived_at FROM squad_history ORDER BY match_date DESC, player_nickname """) details = sql_read(sql_details) return render_template('squad_history.html', history=history, details=details) except Exception as e: flash(f'Error loading squad history: {str(e)}', 'error') return redirect(url_for('admin_dashboard')) @app.route('/admin/squad/remove', methods=['POST']) @basic_auth.required def delPlayerFromSquad(): """Remove player from squad""" _playerNumber = request.args.get('playerNumber') if not _playerNumber: flash('Player number not provided', 'error') return redirect(url_for('match_squad_list')) sql = text("DELETE FROM _hkfcc_matchsquad WHERE playernumber = :player_number") sql_write(sql, {'player_number': _playerNumber}) flash(f'Player #{_playerNumber} removed from squad', 'success') return redirect(url_for('match_squad_list')) @app.route('/admin/squad/reset') @basic_auth.required def matchSquadReset(): """Reset squad for new match - archives current squad to history before clearing""" try: # Get current match date and fixture number from admin settings sql_settings = text("SELECT nextdate, nextfixture FROM motmadminsettings") settings = sql_read_static(sql_settings) if not settings: flash('Error: Admin settings not found. Please configure match settings first.', 'error') return render_template('match_squad_reset.html') match_date = settings[0]['nextdate'] fixture_number = match_date.strftime('%Y%m%d') if match_date else 'unknown' # Check if there are any players in the current squad check_sql = text("SELECT COUNT(*) as count FROM _hkfcc_matchsquad") result = sql_read(check_sql) squad_count = result[0]['count'] if result else 0 if squad_count > 0: # Archive current squad to history table archive_sql = text(""" INSERT INTO squad_history (player_number, player_forenames, player_surname, player_nickname, match_date, fixture_number) SELECT playernumber, playerforenames, playersurname, playernickname, :match_date, :fixture_number FROM _hkfcc_matchsquad """) sql_write(archive_sql, {'match_date': match_date, 'fixture_number': fixture_number}) # Clear current squad table clear_sql = text("DELETE FROM _hkfcc_matchsquad") sql_write(clear_sql) # Update previous fixture number in settings update_sql = text("UPDATE motmadminsettings SET prevfixture = :fixture_number") sql_write_static(update_sql, {'fixture_number': fixture_number}) flash(f'Squad reset successfully! {squad_count} players archived for match {fixture_number} ({match_date})', 'success') else: flash('No players in current squad to reset', 'info') except Exception as e: print(f"Error in squad reset: {str(e)}") flash(f'Error resetting squad: {str(e)}', 'error') return render_template('match_squad_reset.html') @app.route('/admin/stats', methods=['GET', 'POST']) @basic_auth.required def stats_admin(): """Admin page for managing goals and assists statistics""" form = goalsAssistsForm() sql = "SELECT date, homeTeam, awayTeam, venue, fixtureNumber FROM hockeyFixtures WHERE homeTeam='HKFC C' OR awayTeam='HKFC C'" matches = sql_read(sql) form.match.choices = [(match['fixtureNumber'], match['date']) for match in matches] sql2 = "SELECT playerNumber, playerNickname FROM _hkfcC_matchSquad" players = sql_read(sql2) return render_template('goals_assists_admin.html', data=players, form=form) @app.route('/admin/stats/submit', methods=['POST']) @basic_auth.required def goalsAssistsSubmit(): """Process goals and assists statistics""" try: data = request.form playerName = request.form.getlist('playerName') playerNumber = request.form.getlist('playerNumber') assists = request.form.getlist('assists') goals = request.form.getlist('goals') match = request.form['match'] for idx, player in enumerate(playerNumber): sql = "INSERT INTO _hkfc_c_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) except Exception as e: print(e) finally: return render_template('goals_thanks.html', data=data) # ==================== API ENDPOINTS ==================== @app.route('/admin/api/next-fixture') @basic_auth.required def get_nextfixture(): """API endpoint to fetch the next HKFC C fixture from Hockey Hong Kong website""" try: fixture = get_next_hkfc_c_fixture() if fixture: # Get opponent club information opponent_club_info = get_opponent_club_info(fixture['opponent']) # Get the opponent logo URL using S3 service opponent_logo_url = s3_asset_service.get_asset_url('images/default_logo.png') # Default fallback if opponent_club_info and opponent_club_info.get('logo_url'): opponent_logo_url = s3_asset_service.get_logo_url(opponent_club_info['logo_url'], opponent_club_info['club_name']) # Format the fixture data for JSON response fixture_data = { 'success': True, 'date': fixture['date'].strftime('%Y-%m-%d'), 'date_formatted': fixture['date'].strftime('%A, %d %B %Y'), 'time': fixture['time'], 'venue': fixture['venue'], 'opponent': fixture['opponent'], 'opponent_club': get_opponent_club_name(fixture['opponent']), 'opponent_club_info': opponent_club_info, 'opponent_logo_url': opponent_logo_url, 'is_home': fixture['is_home'], 'home_team': fixture['home_team'], 'away_team': fixture['away_team'], 'division': fixture['division'] } return jsonify(fixture_data) else: return jsonify({ 'success': False, 'message': 'No upcoming fixtures found for HKFC C' }) except Exception as e: return jsonify({ 'success': False, 'message': f'Error fetching fixture: {str(e)}' }) @app.route('/admin/api/clubs') @basic_auth.required def get_hockey_clubs(): """API endpoint to fetch clubs from Hockey Hong Kong website""" try: clubs = get_hk_hockey_clubs() if clubs: # Format the club data for JSON response club_data = { 'success': True, 'clubs': clubs, 'count': len(clubs) } return jsonify(club_data) else: return jsonify({ 'success': False, 'message': 'No clubs found on Hockey Hong Kong website' }) except Exception as e: return jsonify({ 'success': False, 'message': f'Error fetching clubs: {str(e)}' }) @app.route('/admin/api/import-clubs', methods=['POST']) @basic_auth.required def import_clubs(): """API endpoint to import clubs from Hockey Hong Kong website""" try: clubs = get_hk_hockey_clubs() imported_count = 0 updated_count = 0 for club in clubs: club_name = club['name'] abbreviation = club.get('abbreviation', '') # Check if club already exists sql_check = text("SELECT id FROM clubs WHERE hockey_club = :club_name") existing = sql_read(sql_check, {'club_name': club_name}) if existing: # Update existing club sql_update = text("UPDATE clubs SET logo_url = :logo_url WHERE hockey_club = :club_name") logo_url = f"/static/images/clubs/{club_name.lower().replace(' ', '_').replace('.', '').replace(',', '')}_logo.png" sql_write(sql_update, {'logo_url': logo_url, 'club_name': club_name}) updated_count += 1 else: # Insert new club sql_insert = text("INSERT INTO clubs (hockey_club, logo_url) VALUES (:club_name, :logo_url)") logo_url = f"/static/images/clubs/{club_name.lower().replace(' ', '_').replace('.', '').replace(',', '')}_logo.png" sql_write(sql_insert, {'club_name': club_name, 'logo_url': logo_url}) imported_count += 1 return jsonify({ 'success': True, 'message': f'Successfully imported {imported_count} new clubs and updated {updated_count} existing clubs', 'imported': imported_count, 'updated': updated_count, 'total': len(clubs) }) except Exception as e: return jsonify({ 'success': False, 'message': f'Error importing clubs: {str(e)}' }) @app.route('/admin/api/match-opponent/') @basic_auth.required def match_opponent(opponent): """API endpoint to match an opponent team to a club in the database""" try: club_info = get_opponent_club_info(opponent) if club_info: return jsonify({ 'success': True, 'opponent': opponent, 'club_info': club_info }) else: return jsonify({ 'success': False, 'message': f'No club found for opponent: {opponent}' }) except Exception as e: return jsonify({ 'success': False, 'message': f'Error matching opponent: {str(e)}' }) @app.route('/admin/api/team/') def admin_team_lookup(club): """API endpoint for team lookup by club""" sql = "SELECT team FROM _clubTeams WHERE club='" + club + "'" myteams = sql_read(sql) return jsonify(myteams) @app.route('/admin/api/logo/') def get_logo(club): """API endpoint for club logo lookup""" sql = "SELECT logoURL FROM mensHockeyClubs WHERE hockeyClub='" + club + "'" clubLogo = sql_read(sql) return jsonify(clubLogo) @app.route('/admin/api/fixture/') def admin_fixture_lookup(fixture): """API endpoint for fixture team lookup""" sql = "SELECT homeTeam, awayTeam FROM hockeyFixtures WHERE fixtureNumber='" + fixture + "'" myteams = sql_read(sql) if not myteams: return jsonify({'error': 'Fixture not found'}) if myteams[0]['homeTeam'].startswith("HKFC"): nextOppo = myteams[0]['awayTeam'] else: nextOppo = myteams[0]['homeTeam'] return jsonify(nextOppo) @app.route('/admin/api/fixture//logo') def admin_fixture_logo_lookup(fixture): """API endpoint for fixture logo lookup""" sql = "SELECT homeTeam, awayTeam FROM hockeyFixtures WHERE fixtureNumber='" + fixture + "'" myteams = sql_read(sql) if not myteams: return jsonify({'error': 'Fixture not found'}) if myteams[0]['homeTeam'].startswith("HKFC C"): nextOppo = myteams[0]['awayTeam'] else: nextOppo = myteams[0]['homeTeam'] sql2 = "SELECT club FROM _clubTeams WHERE displayName ='" + nextOppo + "'" clubs = sql_read_static(sql2) if not clubs: return jsonify({'error': 'Club not found'}) clubName = clubs[0]['club'] sql3 = "SELECT logoUrl FROM mensHockeyClubs WHERE hockeyClub ='" + clubName + "'" logo = sql_read_static(sql3) if not logo: return jsonify({'error': 'Logo not found'}) clubLogo = logo[0]['logoUrl'] return jsonify(clubLogo) @app.route('/api/vote-results') def vote_results(): """API endpoint for voting results""" # Get the current match date from admin settings sql_date = text("SELECT nextdate FROM motmadminsettings") date_result = sql_read_static(sql_date) if not date_result: return json.dumps([]) _matchDate = str(date_result[0]['nextdate']).replace('-', '') print(f"Match date: {_matchDate}") # Query for votes with the current match date sql = text(f"SELECT playername, motm_{_matchDate}, dotd_{_matchDate} FROM _hkfc_c_motm WHERE (motm_{_matchDate} > 0) OR (dotd_{_matchDate} > 0)") print(f"SQL: {sql}") rows = sql_read(sql) print(f"Results: {rows}") # Format the results for the chart formatted_results = [] for row in rows: formatted_results.append({ 'playerName': row['playername'], f'motm_{_matchDate}': row[f'motm_{_matchDate}'], f'dotd_{_matchDate}': row[f'dotd_{_matchDate}'] }) return jsonify(formatted_results) @app.route('/api/poty-results') def poty_results(): """API endpoint for Player of the Year results with dynamic totals""" # Get all players with their fixture-specific columns sql = text("SELECT * FROM _hkfc_c_motm") rows = sql_read(sql) # Calculate dynamic totals for each player results = [] for player in rows: motm_total = 0 dotd_total = 0 # Sum all fixture-specific columns for col_name in player.keys(): if col_name.startswith('motm_') and col_name != 'motmtotal': motm_total += player[col_name] or 0 elif col_name.startswith('dotd_') and col_name != 'dotdtotal': dotd_total += player[col_name] or 0 # Only include players with votes if motm_total > 0 or dotd_total > 0: results.append({ 'playerName': player['playername'], # Fixed field name to match JavaScript 'motmTotal': motm_total, # Fixed field name to match JavaScript 'dotdTotal': dotd_total # Fixed field name to match JavaScript }) print(f"Dynamic POTY Results: {results}") return jsonify(results) @app.route('/admin/voting') @basic_auth.required def voting_chart(): """Admin page for viewing voting charts""" # Get the current match date from admin settings sql_date = text("SELECT nextdate FROM motmadminsettings") date_result = sql_read_static(sql_date) if date_result: matchDate = str(date_result[0]['nextdate']).replace('-', '') else: matchDate = '20251015' # Default fallback return render_template('vote_chart.html', _matchDate=matchDate) @app.route('/admin/poty') @basic_auth.required def poty_chart(): """Admin page for Player of the Year chart""" return render_template('poty_chart.html') # ==================== DATABASE SETUP SECTION ==================== @app.route('/admin/database-setup', methods=['GET', 'POST']) @basic_auth.required def database_setup(): """Admin page for database setup and configuration""" form = DatabaseSetupForm() if request.method == 'POST': if form.test_connection.data: # Test database connection form_data = { 'database_type': form.database_type.data, 'sqlite_database_path': form.sqlite_database_path.data, 'mysql_host': form.mysql_host.data, 'mysql_port': form.mysql_port.data, 'mysql_database': form.mysql_database.data, 'mysql_username': form.mysql_username.data, 'mysql_password': form.mysql_password.data, 'mysql_charset': form.mysql_charset.data, 'postgres_host': form.postgres_host.data, 'postgres_port': form.postgres_port.data, 'postgres_database': form.postgres_database.data, 'postgres_username': form.postgres_username.data, 'postgres_password': form.postgres_password.data, } success, message = db_config_manager.test_connection(form_data) if success: flash(f'✅ {message}', 'success') else: flash(f'❌ {message}', 'error') elif form.save_config.data: # Save configuration form_data = { 'database_type': form.database_type.data, 'sqlite_database_path': form.sqlite_database_path.data, 'mysql_host': form.mysql_host.data, 'mysql_port': form.mysql_port.data, 'mysql_database': form.mysql_database.data, 'mysql_username': form.mysql_username.data, 'mysql_password': form.mysql_password.data, 'mysql_charset': form.mysql_charset.data, 'postgres_host': form.postgres_host.data, 'postgres_port': form.postgres_port.data, 'postgres_database': form.postgres_database.data, 'postgres_username': form.postgres_username.data, 'postgres_password': form.postgres_password.data, } try: db_config_manager.save_config(form_data) flash('✅ Database configuration saved successfully!', 'success') except Exception as e: flash(f'❌ Failed to save configuration: {str(e)}', 'error') elif form.initialize_database.data: # Initialize database try: success, message = db_config_manager.initialize_database( create_sample_data=form.create_sample_data.data ) if success: flash(f'✅ {message}', 'success') else: flash(f'❌ {message}', 'error') except Exception as e: flash(f'❌ Database initialization failed: {str(e)}', 'error') # Load current configuration for display current_config = db_config_manager.get_config_dict() # Populate form with current configuration (only for GET requests or after POST processing) for field_name, value in current_config.items(): if hasattr(form, field_name): getattr(form, field_name).data = value return render_template('database_setup.html', form=form, current_config=current_config) @app.route('/admin/database-status') @basic_auth.required def database_status(): """Admin page showing current database status and configuration""" try: # Reload database configuration to get latest settings from database import DatabaseConfig current_db_config = DatabaseConfig() engine = current_db_config.engine # Test connection with engine.connect() as conn: result = conn.execute(text("SELECT 1")) result.fetchone() connection_status = "✅ Connected" # Get database info db_info = { 'database_url': str(current_db_config.database_url), 'database_type': os.getenv('DATABASE_TYPE', 'unknown'), 'connection_status': connection_status } # Try to get table count try: from database import Base table_count = len(Base.metadata.tables) db_info['table_count'] = table_count except: db_info['table_count'] = 'Unknown' except Exception as e: db_info = { 'database_url': 'Not configured', 'database_type': 'Unknown', 'connection_status': f'❌ Connection failed: {str(e)}', 'table_count': 'Unknown' } return render_template('database_status.html', db_info=db_info) # ==================== S3 CONFIGURATION SECTION ==================== @app.route('/admin/s3-config', methods=['GET', 'POST']) @basic_auth.required def s3_config(): """Admin page for S3 configuration and asset management""" form = S3ConfigForm() if request.method == 'POST': if form.test_connection.data: # Test S3 connection form_data = { 'enable_s3': form.enable_s3.data, 'storage_provider': form.storage_provider.data, 'aws_access_key_id': form.aws_access_key_id.data, 'aws_secret_access_key': form.aws_secret_access_key.data, 'aws_region': form.aws_region.data, 'minio_endpoint': form.minio_endpoint.data, 'minio_use_ssl': form.minio_use_ssl.data, 'bucket_name': form.bucket_name.data, 'bucket_prefix': form.bucket_prefix.data, 'use_signed_urls': form.use_signed_urls.data, 'signed_url_expiry': form.signed_url_expiry.data, 'fallback_to_static': form.fallback_to_static.data } success, message = s3_config_manager.test_connection(form_data) if success: flash(f'✅ {message}', 'success') else: flash(f'❌ {message}', 'error') elif form.save_config.data: # Save S3 configuration form_data = { 'enable_s3': form.enable_s3.data, 'storage_provider': form.storage_provider.data, 'aws_access_key_id': form.aws_access_key_id.data, 'aws_secret_access_key': form.aws_secret_access_key.data, 'aws_region': form.aws_region.data, 'minio_endpoint': form.minio_endpoint.data, 'minio_use_ssl': form.minio_use_ssl.data, 'bucket_name': form.bucket_name.data, 'bucket_prefix': form.bucket_prefix.data, 'use_signed_urls': form.use_signed_urls.data, 'signed_url_expiry': form.signed_url_expiry.data, 'fallback_to_static': form.fallback_to_static.data } try: success = s3_config_manager.save_config(form_data) if success: flash('✅ S3 configuration saved successfully!', 'success') else: flash('❌ Failed to save S3 configuration', 'error') except Exception as e: flash(f'❌ Error saving configuration: {str(e)}', 'error') elif form.cancel.data: return redirect(url_for('admin_dashboard')) # Load current configuration for display current_config = s3_config_manager.get_config_dict() # Populate form with current configuration (only for GET requests or after POST processing) for field_name, value in current_config.items(): if hasattr(form, field_name): getattr(form, field_name).data = value return render_template('s3_config.html', form=form, current_config=current_config) @app.route('/admin/s3-status') @basic_auth.required def s3_status(): """Admin page showing current S3 status and configuration""" try: current_config = s3_config_manager.get_config_dict() # Test current configuration success, message = s3_config_manager.test_connection() s3_info = { 'enabled': current_config.get('enable_s3', False), 'storage_provider': current_config.get('storage_provider', 'aws'), 'bucket_name': current_config.get('bucket_name', 'Not configured'), 'aws_region': current_config.get('aws_region', 'Not configured'), 'minio_endpoint': current_config.get('minio_endpoint', 'Not configured'), 'use_signed_urls': current_config.get('use_signed_urls', True), 'fallback_enabled': current_config.get('fallback_to_static', True), 'connection_status': message, 'connection_success': success } except Exception as e: s3_info = { 'enabled': False, 'storage_provider': 'aws', 'bucket_name': 'Not configured', 'aws_region': 'Not configured', 'minio_endpoint': 'Not configured', 'use_signed_urls': True, 'fallback_enabled': True, 'connection_status': f'Error: {str(e)}', 'connection_success': False } return render_template('s3_status.html', s3_info=s3_info) @app.route('/admin/api/s3-browser') @basic_auth.required def s3_browser(): """API endpoint to browse S3 bucket contents""" try: config = s3_config_manager.config if not config.get('enable_s3', False): return jsonify({ 'success': False, 'message': 'S3 storage is not enabled' }) # Get path parameter (default to root of assets folder) path = request.args.get('path', '') if not path.endswith('/') and path != '': path += '/' # List objects in S3 bucket_name = config.get('bucket_name', '') bucket_prefix = config.get('bucket_prefix', 'assets/') prefix = bucket_prefix + path s3_client = s3_asset_service.s3_client if not s3_client: return jsonify({ 'success': False, 'message': 'S3 client not available' }) # List objects response = s3_client.list_objects_v2( Bucket=bucket_name, Prefix=prefix, Delimiter='/' ) # Process folders folders = [] if 'CommonPrefixes' in response: for folder in response['CommonPrefixes']: folder_name = folder['Prefix'].replace(prefix, '').rstrip('/') if folder_name: # Skip empty folder names folders.append({ 'name': folder_name, 'path': path + folder_name + '/', 'type': 'folder' }) # Process files files = [] if 'Contents' in response: for obj in response['Contents']: file_key = obj['Key'].replace(prefix, '') if file_key and not file_key.endswith('/'): # Skip folders and empty keys # Generate public URL for the file (for preview images) file_url = s3_asset_service.get_asset_url_public(file_key) files.append({ 'name': file_key, 'path': path + file_key, 'url': file_url, 'size': obj['Size'], 'last_modified': obj['LastModified'].isoformat(), 'type': 'file' }) # Sort results folders.sort(key=lambda x: x['name'].lower()) files.sort(key=lambda x: x['name'].lower()) return jsonify({ 'success': True, 'path': path, 'folders': folders, 'files': files, 'parent_path': '/'.join(path.rstrip('/').split('/')[:-1]) + '/' if path != '' else '' }) except Exception as e: return jsonify({ 'success': False, 'message': f'Error browsing S3: {str(e)}' }) if __name__ == "__main__": app.run(host='0.0.0.0', port=5000, debug=True)