# 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 from wtforms import DateField from wtforms.validators import InputRequired, Email, Length from forms import motmForm, adminSettingsForm2, goalsAssistsForm, DatabaseSetupForm, PlayerForm, ClubForm, TeamForm, DataImportForm, ClubSelectionForm 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 app.config['BASIC_AUTH_USERNAME'] = 'admin' app.config['BASIC_AUTH_PASSWORD'] = 'letmein' basic_auth = BasicAuth(app) @app.route('/') def index(): """Main index page for MOTM system""" return render_template('index.html') @app.route('/admin') @basic_auth.required def admin_dashboard(): """Admin dashboard - central hub for all admin functions""" return render_template('admin_dashboard.html') # ==================== 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 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'] hkfcLogo = nextInfo[0]['hkfclogo'] oppoLogo = nextInfo[0]['oppologo'] currMotM = nextInfo[0]['currmotm'] currDotD = nextInfo[0]['currdotd'] oppo = nextTeam # Get opponent club logo from clubs table 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 = club_logo_result[0]['logo_url'] # 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 pictures if nextInfo and nextInfo[0]['currmotm'] and nextInfo[0]['currdotd']: sql3 = text("SELECT playernickname FROM _hkfc_players WHERE playernumber = :curr_motm") sql4 = text("SELECT playernickname FROM _hkfc_players WHERE playernumber = :curr_dotd") motm = sql_read(sql3, {'curr_motm': nextInfo[0]['currmotm']}) dotd = sql_read(sql4, {'curr_dotd': nextInfo[0]['currdotd']}) else: motm = [] dotd = [] # Handle empty results if not motm or not dotd: return render_template('error.html', message="Player data not found. Please set up current MOTM and DotD players in admin settings.") # Use default player images since playerPictureURL column doesn't exist motmURL = '/static/images/default_player.png' dotdURL = '/static/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() # 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, form=form) 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'] commentDate = row[0]['nextdate'].strftime('%Y-%m-%d') _matchDate = row[0]['nextdate'].strftime('%Y_%m_%d') hkfcLogo = row[0]['hkfcLogo'] oppoLogo = row[0]['oppoLogo'] 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: _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 (don't update totals) 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 (don't update totals) 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}) # 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}) return render_template('vote_thanks.html') 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': if form.saveButton.data: print('Saved') else: print('Activated') _nextTeam = request.form.get('nextOppoTeam', '') _nextMatchDate = request.form.get('nextMatchDate', '') _currMotM = request.form.get('currMotM', '0') _currDotD = request.form.get('currDotD', '0') # 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'] # Only update currMotM and currDotD if they were provided if _currMotM and _currMotM != '0' and _currDotD and _currDotD != '0': sql = text("UPDATE motmadminsettings SET nextdate = :next_date, nextClub = :next_club, nextTeam = :next_team, currMotM = :curr_motm, currDotD = :curr_dotd") sql_write_static(sql, { 'next_date': _nextMatchDate, 'next_club': _nextClub, 'next_team': _nextTeam, 'curr_motm': _currMotM, 'curr_dotd': _currDotD }) else: # Don't update currMotM and currDotD if not provided sql = text("UPDATE motmadminsettings SET nextdate = :next_date, nextClub = :next_club, nextTeam = :next_team") sql_write_static(sql, { 'next_date': _nextMatchDate, 'next_club': _nextClub, 'next_team': _nextTeam }) # 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 = :next_club) WHERE nextclub = :next_club") sql_write_static(sql2, {'next_club': _nextClub}) if form.saveButton.data: flash('Settings saved!') urlSuffix = randomUrlSuffix(8) print(urlSuffix) sql3 = text("UPDATE motmadminsettings SET motmurlsuffix = :url_suffix WHERE userid = 'admin'") sql_write_static(sql3, {'url_suffix': urlSuffix}) 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 print(f"Columns already exist for fixture {_nextFixture}: {e}") pass sql5 = text("SELECT motmurlsuffix FROM motmadminsettings WHERE userid = 'admin'") tempSuffix = sql_read_static(sql5) if not tempSuffix: flash('Error: Admin settings not found', 'error') return redirect(url_for('motm_admin')) currSuffix = tempSuffix[0]['motmurlsuffix'] print(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 FROM motmadminsettings WHERE userid = 'admin'") 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 sql4 = text("SELECT hockeyclub FROM menshockeyclubs ORDER BY hockeyclub") sql5 = text("SELECT nextclub, oppologo FROM motmadminsettings") sql6 = text(f"SELECT playernumber, playerforenames, playersurname FROM _hkfcc_matchsquad_{prevFixture} ORDER BY playerforenames") 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] form.currMotM.choices = [(player['playernumber'], player['playerforenames'] + " " + player['playersurname']) for player in players] form.currDotD.choices = [(player['playernumber'], player['playerforenames'] + " " + player['playersurname']) for player in players] clubLogo = settings[0]['oppologo'] 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) @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/hkfc_logo.png'}, {'hockey_club': 'KCC', 'logo_url': '/static/images/kcc_logo.png'}, {'hockey_club': 'USRC', 'logo_url': '/static/images/usrc_logo.png'}, {'hockey_club': 'Valley', 'logo_url': '/static/images/valley_logo.png'}, {'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/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/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/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""" _matchNumber = str(mySettings('fixture')) print(_matchNumber) try: # First, 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: # Rename current squad table sql1 = text(f"RENAME TABLE _hkfcC_matchSquad TO _hkfcC_matchSquad_{_matchNumber}") sql_write(sql1) # Create new empty squad table sql2 = text("CREATE TABLE _hkfcC_matchSquad (playerNumber smallint UNIQUE, playerForenames varchar(50), playerSurname varchar(30), playerNickname varchar(30) NOT NULL, PRIMARY KEY (playerNumber))") sql_write(sql2) # Update fixture number sql3 = text("UPDATE motmAdminSettings SET prevFixture = :match_number") sql_write_static(sql3, {'match_number': _matchNumber}) flash(f'Squad reset successfully! {squad_count} players archived for match {_matchNumber}', 'success') else: flash('No players in current squad to reset', 'info') except Exception as 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_next_fixture(): """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']) # 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, '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 WHERE userid = 'admin'") 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'], 'motmtotal': motm_total, 'dotdtotal': dotd_total }) 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 WHERE userid = 'admin'") date_result = sql_read_static(sql_date) if date_result: matchDate = str(date_result[0]['nextdate']).replace('-', '') else: matchDate = '20251012' # 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) if __name__ == "__main__": app.run(host='0.0.0.0', port=5000, debug=True)