From 7f0d171e219dd424c1e93f6ec8ea92c4e2d8c1e6 Mon Sep 17 00:00:00 2001 From: Jonny Ervine Date: Wed, 1 Oct 2025 23:25:05 +0800 Subject: [PATCH] Latest updates to use postgres --- motm_app/club_scraper.py | 237 ++++++++ motm_app/fixture_scraper.py | 328 +++++++++++ motm_app/main.py | 548 +++++++++++++++---- motm_app/readSettings.py | 7 +- motm_app/requirements.txt | 5 + motm_app/templates/admin_dashboard.html | 186 +++++++ motm_app/templates/club_management.html | 223 ++++++++ motm_app/templates/error.html | 2 +- motm_app/templates/match_squad.html | 8 +- motm_app/templates/match_squad_selected.html | 26 +- motm_app/templates/motm_admin.html | 103 +++- motm_app/templates/player_management.html | 16 +- 12 files changed, 1546 insertions(+), 143 deletions(-) create mode 100644 motm_app/club_scraper.py create mode 100644 motm_app/fixture_scraper.py create mode 100644 motm_app/templates/admin_dashboard.html diff --git a/motm_app/club_scraper.py b/motm_app/club_scraper.py new file mode 100644 index 0000000..58f727f --- /dev/null +++ b/motm_app/club_scraper.py @@ -0,0 +1,237 @@ +# encoding=utf-8 +""" +Club scraper for Hong Kong Hockey Association website +Fetches men's hockey clubs from https://hockey.org.hk +""" + +import requests +from bs4 import BeautifulSoup +import re + + +class ClubScraper: + """Scrapes club data from Hong Kong Hockey Association website""" + + CLUBS_URL = "https://hockey.org.hk/Content.asp?Uid=27" + + # Common club abbreviations and their full names + CLUB_ABBREVIATIONS = { + 'Pak': 'Pakistan Association of HK Ltd.', + 'KCC': 'Kowloon Cricket Club', + 'HKFC': 'Hong Kong Football Club', + 'USRC': 'United Services Recreation Club', + 'Valley': 'Valley Fort Sports Club', + 'SSSC': 'South China Sports Club', + 'Dragons': 'Dragons Hockey Club', + 'Kai Tak': 'Kai Tak Sports Club', + 'RHOBA': 'Royal Hong Kong Regiment Officers and Businessmen Association', + 'Elite': 'Elite Hockey Club', + 'Aquila': 'Aquila Hockey Club', + 'HKJ': 'Hong Kong Jockey Club', + 'Sirius': 'Sirius Hockey Club', + 'Shaheen': 'Shaheen Hockey Club', + 'Diocesan': 'Diocesan Boys School', + 'Rhino': 'Rhino Hockey Club', + 'Khalsa': 'Khalsa Hockey Club', + 'HKCC': 'Hong Kong Cricket Club', + 'Police': 'Hong Kong Police Force', + 'Recreio': 'Recreio Hockey Club', + 'CSD': 'Correctional Services Department', + 'Dutch': 'Dutch Hockey Club', + 'HKUHC': 'Hong Kong University Hockey Club', + 'Kaitiaki': 'Kaitiaki Hockey Club', + 'Antlers': 'Antlers Hockey Club', + 'Marcellin': 'Marcellin Hockey Club', + 'Skyers': 'Skyers Hockey Club', + 'JR': 'JR Hockey Club', + 'IUHK': 'International University of Hong Kong', + '144U': '144 United Hockey Club', + 'HKU': 'Hong Kong University', + 'UBSC': 'United Brother Sports Club', + 'Nanki': 'Nanki Sports Club', + 'Gojra': 'Gojra Hockey Club', + 'KNS': 'KNS Hockey Club', + 'Hockey Clube de Macau': 'Hockey Clube de Macau' + } + + def __init__(self): + self.session = requests.Session() + self.session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + }) + + def fetch_clubs(self): + """Fetch and parse clubs from the website""" + try: + response = self.session.get(self.CLUBS_URL, timeout=10) + response.raise_for_status() + return self._parse_clubs(response.text) + except requests.RequestException as e: + print(f"Error fetching clubs: {e}") + return [] + + def _parse_clubs(self, html_content): + """Parse HTML content and extract club information""" + soup = BeautifulSoup(html_content, 'lxml') + clubs = [] + + # Look for tables or structured data containing club information + tables = soup.find_all('table') + + for table in tables: + rows = table.find_all('tr') + for row in rows: + cells = row.find_all(['td', 'th']) + if len(cells) >= 2: + # Extract club name from first cell + club_name = cells[0].get_text(strip=True) + + # Skip header rows and empty cells + if not club_name or club_name.lower() in ['club', 'name', 'abbreviation', 'team', 'clubs']: + continue + + # Skip if it's clearly a header row + if club_name == 'Clubs' and abbreviation == 'Abbreviated Title': + continue + + # Extract abbreviation if available + abbreviation = None + if len(cells) > 1: + abbreviation = cells[1].get_text(strip=True) + + # Extract teams if available + teams = [] + if len(cells) > 2: + teams_text = cells[2].get_text(strip=True) + # Parse teams (e.g., "A, B" or "A B") + if teams_text: + teams = [team.strip() for team in re.split(r'[,;]', teams_text) if team.strip()] + + # Extract convenor if available + convenor = None + if len(cells) > 3: + convenor = cells[3].get_text(strip=True) + + # Extract email if available + email = None + if len(cells) > 4: + email = cells[4].get_text(strip=True) + + club_data = { + 'name': club_name, + 'abbreviation': abbreviation, + 'teams': teams, + 'convenor': convenor, + 'email': email + } + clubs.append(club_data) + + # If no structured data found, try to extract from text content + if not clubs: + clubs = self._extract_clubs_from_text(html_content) + + return clubs + + def _extract_clubs_from_text(self, html_content): + """Extract club names from text content if no structured data found""" + soup = BeautifulSoup(html_content, 'lxml') + clubs = [] + + # Look for common patterns in text + text_content = soup.get_text() + + # Extract known club names from the text + for abbreviation, full_name in self.CLUB_ABBREVIATIONS.items(): + if abbreviation in text_content or full_name in text_content: + clubs.append({ + 'name': full_name, + 'abbreviation': abbreviation, + 'teams': [], + 'convenor': None, + 'email': None + }) + + return clubs + + def get_clubs_with_abbreviations(self): + """Get clubs with proper abbreviation handling""" + clubs = self.fetch_clubs() + + # Process clubs to handle abbreviations + processed_clubs = [] + + for club in clubs: + name = club['name'] + abbreviation = club.get('abbreviation', '') + + # If we have an abbreviation, check if it's in our mapping + if abbreviation and abbreviation in self.CLUB_ABBREVIATIONS: + full_name = self.CLUB_ABBREVIATIONS[abbreviation] + processed_club = club.copy() + processed_club['name'] = full_name + processed_club['abbreviation'] = abbreviation + processed_clubs.append(processed_club) + elif name in self.CLUB_ABBREVIATIONS.values(): + # If the name is already a full name, find its abbreviation + for abbr, full in self.CLUB_ABBREVIATIONS.items(): + if full == name: + processed_club = club.copy() + processed_club['abbreviation'] = abbr + processed_clubs.append(processed_club) + break + else: + # Keep as-is if no mapping found + processed_clubs.append(club) + + return processed_clubs + + def get_club_logo_url(self, club_name): + """Generate a logo URL for a club (placeholder implementation)""" + # This could be enhanced to fetch actual logos from the website + # For now, return a placeholder + club_slug = club_name.lower().replace(' ', '_').replace('.', '').replace(',', '') + return f"/static/images/clubs/{club_slug}_logo.png" + + +def get_hk_hockey_clubs(): + """Convenience function to get Hong Kong hockey clubs""" + scraper = ClubScraper() + return scraper.get_clubs_with_abbreviations() + + +def expand_club_abbreviation(abbreviation): + """Expand a club abbreviation to its full name""" + return ClubScraper.CLUB_ABBREVIATIONS.get(abbreviation, abbreviation) + + +if __name__ == "__main__": + # Test the scraper + print("Testing Hong Kong Hockey Club Scraper...") + print("=" * 60) + + scraper = ClubScraper() + + print("\nFetching clubs from Hockey Hong Kong website...") + clubs = scraper.get_clubs_with_abbreviations() + + if clubs: + print(f"\nFound {len(clubs)} clubs:") + for i, club in enumerate(clubs, 1): + print(f"\n{i}. {club['name']}") + if club.get('abbreviation'): + print(f" Abbreviation: {club['abbreviation']}") + if club.get('teams'): + print(f" Teams: {', '.join(club['teams'])}") + if club.get('convenor'): + print(f" Convenor: {club['convenor']}") + if club.get('email'): + print(f" Email: {club['email']}") + else: + print("\nNo clubs found. This might be due to website structure changes.") + print("Using fallback club list...") + + # Fallback to known clubs + for abbreviation, full_name in scraper.CLUB_ABBREVIATIONS.items(): + print(f"- {full_name} ({abbreviation})") + + print("\n" + "=" * 60) diff --git a/motm_app/fixture_scraper.py b/motm_app/fixture_scraper.py new file mode 100644 index 0000000..c70c194 --- /dev/null +++ b/motm_app/fixture_scraper.py @@ -0,0 +1,328 @@ +# encoding=utf-8 +""" +Fixture scraper for Hong Kong Hockey Association website +Fetches upcoming HKFC C team fixtures from https://hockey.org.hk/MenFixture.asp +""" + +import requests +from bs4 import BeautifulSoup +from datetime import datetime +import re + + +class FixtureScraper: + """Scrapes fixture data from Hong Kong Hockey Association website""" + + FIXTURE_URL = "https://hockey.org.hk/MenFixture.asp" + TARGET_TEAM = "HKFC C" + + def __init__(self): + self.session = requests.Session() + self.session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + }) + + def fetch_fixtures(self): + """Fetch and parse fixtures from the website""" + try: + response = self.session.get(self.FIXTURE_URL, timeout=10) + response.raise_for_status() + return self._parse_fixtures(response.text) + except requests.RequestException as e: + print(f"Error fetching fixtures: {e}") + return [] + + def _parse_fixtures(self, html_content): + """Parse HTML content and extract fixture information""" + soup = BeautifulSoup(html_content, 'lxml') + fixtures = [] + + # Find all table rows + rows = soup.find_all('tr') + current_date = None + + for row in rows: + # Check if this row contains a date header + date_cells = row.find_all('td', colspan=True) + if date_cells: + date_text = date_cells[0].get_text(strip=True) + # Extract date from text like "Sunday, 7 Sep 2025" + date_match = re.search(r'(\w+day),\s+(\d+)\s+(\w+)\s+(\d{4})', date_text) + if date_match: + try: + day_name, day, month, year = date_match.groups() + date_str = f"{day} {month} {year}" + current_date = datetime.strptime(date_str, "%d %b %Y").date() + except ValueError: + continue + continue + + # Check if this row contains fixture data + cells = row.find_all('td') + if len(cells) >= 5 and current_date: + try: + # Extract fixture details + # Note: The first cell might be empty or contain status (C/P) + # Column order: [Status/Division], Division, Time, Venue, Home, Away, [Umpire columns...] + + # Handle tables with or without status column + if len(cells) >= 6: + # If 6+ columns, likely has status column first + status_or_div = cells[0].get_text(strip=True) + division = cells[1].get_text(strip=True) if cells[1] else "" + time = cells[2].get_text(strip=True) if cells[2] else "" + venue = cells[3].get_text(strip=True) if cells[3] else "" + home_team = cells[4].get_text(strip=True) if cells[4] else "" + away_team = cells[5].get_text(strip=True) if cells[5] else "" + else: + # If 5 columns, no status column + division = cells[0].get_text(strip=True) if cells[0] else "" + time = cells[1].get_text(strip=True) if cells[1] else "" + venue = cells[2].get_text(strip=True) if cells[2] else "" + home_team = cells[3].get_text(strip=True) if cells[3] else "" + away_team = cells[4].get_text(strip=True) if cells[4] else "" + + # Check if HKFC C is playing in this match + if self.TARGET_TEAM in home_team or self.TARGET_TEAM in away_team: + # Determine opponent + if self.TARGET_TEAM in home_team: + opponent = away_team + is_home = True + else: + opponent = home_team + is_home = False + + fixture = { + 'date': current_date, + 'time': time, + 'venue': venue, + 'opponent': opponent, + 'is_home': is_home, + 'home_team': home_team, + 'away_team': away_team, + 'division': division + } + fixtures.append(fixture) + except (IndexError, AttributeError) as e: + # Skip malformed rows + continue + + return fixtures + + def get_next_fixture(self): + """Get the next upcoming HKFC C fixture""" + fixtures = self.fetch_fixtures() + + if not fixtures: + return None + + # Filter for future fixtures and sort by date + today = datetime.now().date() + future_fixtures = [f for f in fixtures if f['date'] >= today] + + if not future_fixtures: + return None + + # Sort by date and return the earliest + future_fixtures.sort(key=lambda x: x['date']) + return future_fixtures[0] + + def get_all_future_fixtures(self, limit=10): + """Get all future HKFC C fixtures, optionally limited""" + fixtures = self.fetch_fixtures() + + if not fixtures: + return [] + + # Filter for future fixtures and sort by date + today = datetime.now().date() + future_fixtures = [f for f in fixtures if f['date'] >= today] + future_fixtures.sort(key=lambda x: x['date']) + + return future_fixtures[:limit] if limit else future_fixtures + + +def get_next_hkfc_c_fixture(): + """Convenience function to get the next HKFC C fixture""" + scraper = FixtureScraper() + return scraper.get_next_fixture() + + +def get_opponent_club_name(opponent_team): + """Extract club name from opponent team name (e.g., 'KCC B' -> 'KCC')""" + if not opponent_team: + return None + + # Common patterns: "Club Letter" (e.g., "KCC B", "Valley A") + # Remove team letters and common suffixes + club_name = re.sub(r'\s+[A-H]$', '', opponent_team).strip() + + return club_name + + +def match_opponent_to_club(opponent_team, clubs_database=None): + """ + Match an opponent team name to a club in the database + + Args: + opponent_team (str): The opponent team name (e.g., "KCC B", "Valley A") + clubs_database (list): List of clubs from database, if None will fetch from DB + + Returns: + dict: Club information if matched, None if no match found + """ + if not opponent_team: + return None + + # Import here to avoid circular imports + try: + from db_config import sql_read + from sqlalchemy import text + except ImportError: + return None + + # Get clubs from database if not provided + if clubs_database is None: + try: + clubs_result = sql_read(text("SELECT hockey_club FROM clubs ORDER BY hockey_club")) + clubs_database = [club['hockey_club'] for club in clubs_result] if clubs_result else [] + except: + clubs_database = [] + + # Extract potential club name from opponent team + potential_club_names = [] + + # Method 1: Remove team letters (A, B, C, etc.) + base_name = re.sub(r'\s+[A-H]$', '', opponent_team).strip() + potential_club_names.append(base_name) + + # Method 2: Remove common suffixes + suffixes_to_remove = [' A', ' B', ' C', ' D', ' E', ' F', ' G', ' H', ' I', ' J'] + for suffix in suffixes_to_remove: + if opponent_team.endswith(suffix): + potential_club_names.append(opponent_team[:-len(suffix)].strip()) + + # Method 3: Split on spaces and try different combinations + words = opponent_team.split() + if len(words) > 1: + # Try first word only + potential_club_names.append(words[0]) + # Try first two words + if len(words) > 2: + potential_club_names.append(' '.join(words[:2])) + + # Try to match against database clubs + for potential_name in potential_club_names: + # Exact match + for club in clubs_database: + if club.lower() == potential_name.lower(): + return { + 'club_name': club, + 'match_type': 'exact', + 'confidence': 'high' + } + + # Partial match (club name contains the potential name) + for club in clubs_database: + if potential_name.lower() in club.lower() or club.lower() in potential_name.lower(): + return { + 'club_name': club, + 'match_type': 'partial', + 'confidence': 'medium' + } + + # If no match found, return the best guess + best_guess = potential_club_names[0] if potential_club_names else opponent_team + return { + 'club_name': best_guess, + 'match_type': 'guess', + 'confidence': 'low' + } + + +def get_opponent_club_info(opponent_team): + """ + Get full club information for an opponent team + + Args: + opponent_team (str): The opponent team name + + Returns: + dict: Full club information including logo URL, or None if not found + """ + if not opponent_team: + return None + + try: + from db_config import sql_read + from sqlalchemy import text + except ImportError: + return None + + # First, try to match the opponent to a club + match_result = match_opponent_to_club(opponent_team) + + if not match_result: + return None + + club_name = match_result['club_name'] + + # Get full club information from database + try: + sql = text("SELECT id, hockey_club, logo_url FROM clubs WHERE hockey_club = :club_name") + club_info = sql_read(sql, {'club_name': club_name}) + + if club_info: + club_data = club_info[0] + return { + 'id': club_data['id'], + 'club_name': club_data['hockey_club'], + 'logo_url': club_data['logo_url'], + 'match_result': match_result + } + else: + # Club not found in database, return match result only + return { + 'club_name': club_name, + 'logo_url': None, + 'match_result': match_result + } + except Exception as e: + print(f"Error getting club info: {e}") + return None + + +if __name__ == "__main__": + # Test the scraper + print("Testing Hong Kong Hockey Fixture Scraper...") + print("=" * 60) + + scraper = FixtureScraper() + + print("\nFetching next HKFC C fixture...") + next_fixture = scraper.get_next_fixture() + + if next_fixture: + print(f"\nNext HKFC C Match:") + print(f" Date: {next_fixture['date'].strftime('%A, %d %B %Y')}") + print(f" Time: {next_fixture['time']}") + print(f" Venue: {next_fixture['venue']}") + print(f" Opponent: {next_fixture['opponent']}") + print(f" Home/Away: {'Home' if next_fixture['is_home'] else 'Away'}") + print(f" Division: {next_fixture['division']}") + + club_name = get_opponent_club_name(next_fixture['opponent']) + print(f" Opponent Club: {club_name}") + else: + print("\nNo upcoming fixtures found.") + + print("\n" + "=" * 60) + print("\nFetching next 5 HKFC C fixtures...") + future_fixtures = scraper.get_all_future_fixtures(limit=5) + + if future_fixtures: + for i, fixture in enumerate(future_fixtures, 1): + print(f"\n{i}. {fixture['date'].strftime('%d %b %Y')} vs {fixture['opponent']} ({fixture['venue']})") + else: + print("\nNo upcoming fixtures found.") + diff --git a/motm_app/main.py b/motm_app/main.py index 04b4a41..0cda581 100644 --- a/motm_app/main.py +++ b/motm_app/main.py @@ -6,6 +6,18 @@ 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 @@ -20,7 +32,8 @@ 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 db_setup import db_config_manager +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' @@ -33,13 +46,20 @@ def index(): 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 = "SELECT playerNumber, playerForenames, playerSurname, playerNickname FROM _hkfcC_matchSquad ORDER BY RAND()" - sql2 = "SELECT nextClub, nextTeam, nextDate, oppoLogo, hkfcLogo, currMotM, currDotD, nextFixture FROM motmAdminSettings" + 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) @@ -47,54 +67,63 @@ def motm_vote(randomUrlSuffix): 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'] + 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 - sql3 = "SELECT hockeyResults2021.hockeyFixtures.date, hockeyResults.motmAdminSettings.nextFixture FROM hockeyResults2021.hockeyFixtures INNER JOIN hockeyResults.motmAdminSettings ON hockeyResults2021.hockeyFixtures.fixtureNumber = hockeyResults.motmAdminSettings.nextFixture" - nextMatchDate = sql_read(sql3) - if not nextMatchDate: - return render_template('error.html', message="No fixtures found. Please initialize the database with sample data.") - nextDate = nextMatchDate[0]['date'] - formatDate = datetime.strftime(nextDate, '%A, %d %B %Y') + # 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.") - sql3 = "SELECT playerPictureURL FROM _HKFC_players INNER JOIN hockeyResults.motmAdminSettings ON _HKFC_players.playerNumber=hockeyResults.motmAdminSettings.currMotM" - sql4 = "SELECT playerPictureURL FROM _HKFC_players INNER JOIN hockeyResults.motmAdminSettings ON _HKFC_players.playerNumber=hockeyResults.motmAdminSettings.currDotD" - motm = sql_read(sql3) - dotd = sql_read(sql4) + # 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 initialize the database with sample data.") + return render_template('error.html', message="Player data not found. Please set up current MOTM and DotD players in admin settings.") - motmURL = motm[0]['playerPictureURL'] - dotdURL = dotd[0]['playerPictureURL'] + # Use default player images since playerPictureURL column doesn't exist + motmURL = '/static/images/default_player.png' + dotdURL = '/static/images/default_player.png' - sql5 = "SELECT comment FROM _motmComments INNER JOIN hockeyResults.motmAdminSettings ON _motmComments.matchDate=hockeyResults.motmAdminSettings.nextDate ORDER BY RAND() LIMIT 1" - comment = sql_read(sql5) - if comment == "": - comment = "No comments added yet" + # 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']}) + comment = comment_result[0]['comment'] if comment_result else "No comments added yet" + form = motmForm() - sql6 = "SELECT motmUrlSuffix FROM hockeyResults.motmAdminSettings WHERE userid='admin'" - urlSuff = sql_read_static(sql6) - if not urlSuff: - return render_template('error.html', message="Admin settings not found. Please initialize the database.") - randomSuff = urlSuff[0]['motmUrlSuffix'] - print(randomSuff) - if randomSuff == randomUrlSuffix: - return render_template('motm_vote.html', data=rows, comment=comment, formatDate=formatDate, matchNumber=nextFixture, oppo=oppo, hkfcLogo=hkfcLogo, oppoLogo=oppoLogo, dotdURL=dotdURL, motmURL=motmURL, form=form) + + # Verify URL suffix + if nextInfo and nextInfo[0].get('motmurlsuffix'): + randomSuff = nextInfo[0]['motmurlsuffix'] + if randomSuff == randomUrlSuffix: + return render_template('motm_vote.html', data=rows, comment=comment, formatDate=formatDate, matchNumber=nextInfo[0].get('nextfixture', ''), 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') + 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 = "SELECT nextClub, nextTeam, nextDate, oppoLogo, hkfcLogo FROM motmAdminSettings" + 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.") @@ -108,10 +137,10 @@ def match_comments(): _comment = request.form['matchComment'] if _comment != 'Optional comments added here': _fixed_comment = _comment.replace("'", "\\'") - sql3 = "INSERT INTO _motmComments (matchDate, opposition, comment) VALUES ('" + commentDate + "', '" + _oppo + "', '" + _fixed_comment + "')" - sql_write(sql3) - sql = "SELECT comment FROM _motmComments WHERE matchDate='" + _matchDate + "' ORDER BY RAND()" - comments = sql_read(sql) + sql3 = text("INSERT INTO _motmcomments (matchDate, opposition, comment) VALUES (:comment_date, :opposition, :comment)") + sql_write(sql3, {'comment_date': commentDate, 'opposition': _oppo, '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) @@ -127,17 +156,56 @@ def vote_thanks(): _oppo = request.form['oppo'] if _motm and _dotd and request.method == 'POST': - sql = "INSERT INTO _hkfc_c_motm (playerNumber, playerName, motmTotal, motm_" + _matchDate + ") SELECT playerNumber, playerNickname, '1', '1' FROM _HKFC_players WHERE playerNumber='" + _motm + "' ON DUPLICATE KEY UPDATE motmTotal = motmTotal + 1, motm_" + _matchDate + " = motm_" + _matchDate + " + 1" - sql2 = "INSERT INTO _hkfc_c_motm (playerNumber, playerName, dotdTotal, dotd_" + _matchDate + ") SELECT playerNumber, playerNickname, '1', '1' FROM _HKFC_players WHERE playerNumber='" + _dotd + "' ON DUPLICATE KEY UPDATE dotdTotal = dotdTotal + 1, dotd_" + _matchDate + " = dotd_" + _matchDate + " + 1" - if _comments == "": - print("No comment") - elif _comments == "Optional comments added here": - print("No comment") - else: - sql3 = "INSERT INTO _motmComments (_matchDate, opposition, comment) VALUES ('" + _matchDate + "', '" + _oppo + "', '" + _fixed_comments + "')" - sql_write(sql3) - sql_write(sql) - sql_write(sql2) + # 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, motmtotal, {motm_col}) + VALUES (:player_num, :player_name, 1, 1) + ON CONFLICT (playernumber) DO UPDATE SET + motmTotal = _hkfc_c_motm.motmTotal + 1, + {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, dotdtotal, {dotd_col}) + VALUES (:player_num, :player_name, 1, 1) + ON CONFLICT (playernumber) DO UPDATE SET + dotdTotal = _hkfc_c_motm.dotdTotal + 1, + {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, opposition, comment) VALUES (:match_date, :opposition, :comment)") + sql_write(sql3, {'match_date': _matchDate, 'opposition': _oppo, 'comment': _fixed_comments}) + return render_template('vote_thanks.html') else: return 'Ouch ... something went wrong here' @@ -164,38 +232,81 @@ def motm_admin(): print('Saved') else: print('Activated') - _nextTeam = request.form['nextOppoTeam'] - _nextMatchDate = request.form['nextMatchDate'] - _currMotM = request.form['currMotM'] - _currDotD = request.form['currDotD'] - sql1 = "SELECT club FROM _clubTeams WHERE displayName='" + _nextTeam + "'" - _nextClubName = sql_read_static(sql1) - if not _nextClubName: - flash('Error: Club not found for team ' + _nextTeam, 'error') + _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')) - _nextClub = _nextClubName[0]['club'] - sql = "UPDATE motmAdminSettings SET nextDate='" + _nextMatchDate + "', nextClub='" + _nextClub + "', nextTeam='" + _nextTeam + "', currMotM=" + _currMotM + ", currDotD=" + _currDotD + "" - sql_write_static(sql) - sql2 = "UPDATE motmAdminSettings INNER JOIN mensHockeyClubs ON motmAdminSettings.nextClub = mensHockeyClubs.hockeyClub SET motmAdminSettings.oppoLogo = mensHockeyClubs.logoURL WHERE mensHockeyClubs.hockeyClub='" + _nextClub + "'" - sql_write_static(sql2) + + # 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 = "UPDATE motmAdminSettings SET motmUrlSuffix='" + urlSuffix + "' WHERE userid='admin'" - sql_write_static(sql3) + sql3 = text("UPDATE motmadminsettings SET motmurlsuffix = :url_suffix WHERE userid = 'admin'") + sql_write_static(sql3, {'url_suffix': urlSuffix}) flash('MotM URL https://hockey.ervine.cloud/motm/'+urlSuffix) elif form.activateButton.data: # Generate a fixture number based on the date _nextFixture = _nextMatchDate.replace('-', '') - sql4 = "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) - sql5 = "SELECT motmUrlSuffix FROM motmAdminSettings WHERE userid='admin'" + 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'] + currSuffix = tempSuffix[0]['motmurlsuffix'] print(currSuffix) flash('Man of the Match vote is now activated') flash('MotM URL https://hockey.ervine.cloud/motm/'+currSuffix) @@ -203,18 +314,20 @@ def motm_admin(): flash('Something went wrong - check with Smithers') # Load current settings to populate the form - sql_current = "SELECT nextDate FROM motmAdminSettings WHERE userid='admin'" + 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 = datetime.strptime(current_settings[0]['nextDate'], '%Y-%m-%d').date() + 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 = "SELECT hockeyClub FROM mensHockeyClubs ORDER BY hockeyClub" - sql5 = "SELECT nextClub, oppoLogo FROM motmAdminSettings" - sql6 = "SELECT playerNumber, playerForenames, playerSurname FROM _hkfcC_matchSquad_" + prevFixture + " ORDER BY playerForenames" + 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) @@ -223,14 +336,14 @@ def motm_admin(): if not clubs: clubs = [] if not settings: - settings = [{'nextClub': 'Unknown', 'oppoLogo': '/static/images/default_logo.png'}] + 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'] + 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) @@ -239,7 +352,7 @@ def motm_admin(): @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") + 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) @@ -253,7 +366,7 @@ def add_player(): 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") + 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: @@ -261,7 +374,7 @@ def add_player(): 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 = 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, @@ -285,7 +398,7 @@ def edit_player(player_number): if request.method == 'GET': # Load player data - sql = text("SELECT playerNumber, playerForenames, playerSurname, playerNickname, playerTeam FROM _HKFC_players WHERE playerNumber = :player_number") + 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: @@ -293,16 +406,16 @@ def edit_player(player_number): 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'] + 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 = 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, @@ -322,7 +435,7 @@ def edit_player(player_number): @basic_auth.required def delete_player(player_number): """Delete a player""" - sql = text("DELETE FROM _HKFC_players WHERE playerNumber = :player_number") + 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')) @@ -332,7 +445,7 @@ def delete_player(player_number): @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") + 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) @@ -672,11 +785,11 @@ def data_import(): for player_data in players_data: # Check if player already exists - sql_check = text("SELECT playerNumber FROM _HKFC_players WHERE playerNumber = :player_number") + 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 = 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 @@ -693,10 +806,18 @@ def data_import(): 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 = "INSERT INTO _hkfcC_matchSquad (playerNumber, playerForenames, playerSurname, playerNickname) SELECT playerNumber, playerForenames, playerSurname, playerNickname FROM _HKFC_players WHERE playerNumber='" + _playerNumber + "'" - sql_write(sql) - sql2 = "SELECT playerNumber, playerForenames, playerSurname, playerNickname FROM _hkfcC_matchSquad" + 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 @@ -708,7 +829,7 @@ def match_squad_submit(): @basic_auth.required def match_squad_list(): """Display current squad list""" - sql = "SELECT playerNumber, playerForenames, playerSurname, playerNickname FROM _hkfcC_matchSquad" + sql = text("SELECT playernumber, playerforenames, playersurname, playernickname FROM _hkfcc_matchsquad") players = sql_read(sql) table = matchSquadTable(players) table.border = True @@ -720,10 +841,15 @@ def match_squad_list(): @basic_auth.required def delPlayerFromSquad(): """Remove player from squad""" - _playerNumber = request.args['playerNumber'] - sql = "DELETE FROM _hkfcC_matchSquad WHERE playerNumber=" + _playerNumber + "" - sql_write(sql) - return render_template('player_removed.html', number=_playerNumber) + _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') @@ -732,12 +858,33 @@ def matchSquadReset(): """Reset squad for new match""" _matchNumber = str(mySettings('fixture')) print(_matchNumber) - sql1 = "RENAME TABLE _hkfcC_matchSquad TO _hkfcC_matchSquad_" + _matchNumber + "" - sql2 = "CREATE TABLE _hkfcC_matchSquad (playerNumber smallint UNIQUE, playerForenames varchar(50), playerSurname varchar(30), playerNickname varchar(30) NOT NULL, PRIMARY KEY (playerNumber))" - sql3 = "UPDATE motmAdminSettings SET prevFixture='" + _matchNumber + "'" - sql_write(sql1) - sql_write(sql2) - sql_write_static(sql3) + + 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') @@ -776,6 +923,141 @@ def goalsAssistsSubmit(): # ==================== 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""" @@ -833,29 +1115,57 @@ def admin_fixture_logo_lookup(fixture): @app.route('/api/vote-results') def vote_results(): """API endpoint for voting results""" - _matchDate = str(mySettings('fixture')) - print(_matchDate) - sql = "SELECT playerName, motm_" + _matchDate + ", dotd_" + _matchDate + " FROM _hkfc_c_motm WHERE (motm_" + _matchDate + " > '0') OR (dotd_" + _matchDate + " > '0')" - print(sql) + # 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(rows) - return json.dumps(rows) + 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""" - sql = "SELECT playerName, motmTotal, dotdTotal FROM _hkfc_c_motm WHERE (motmTotal > '0') OR (dotdTotal > '0')" - print(sql) + sql = text("SELECT playername, motmtotal, dotdtotal FROM _hkfc_c_motm WHERE (motmtotal > 0) OR (dotdtotal > 0)") + print(f"SQL: {sql}") rows = sql_read(sql) - return json.dumps(rows) + print(f"Results: {rows}") + return jsonify(rows) @app.route('/admin/voting') @basic_auth.required def voting_chart(): """Admin page for viewing voting charts""" - matchDate = mySettings('fixture') + # 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 = date_result[0]['nextDate'].replace('-', '') + else: + matchDate = '20251012' # Default fallback + return render_template('vote_chart.html', _matchDate=matchDate) diff --git a/motm_app/readSettings.py b/motm_app/readSettings.py index f019af9..e6b3d84 100644 --- a/motm_app/readSettings.py +++ b/motm_app/readSettings.py @@ -2,13 +2,16 @@ import pymysql import os from db_config import sql_read_static +from sqlalchemy import text def mySettings(setting): try: - sql = "SELECT " + setting + " FROM motmAdminSettings WHERE userid='admin'" + # Convert setting to lowercase for PostgreSQL compatibility + setting_lower = setting.lower() + sql = text("SELECT " + setting_lower + " FROM motmadminsettings WHERE userid='admin'") rows = sql_read_static(sql) if rows: - return rows[0][setting] + return rows[0][setting_lower] else: return None except Exception as e: diff --git a/motm_app/requirements.txt b/motm_app/requirements.txt index e4dd7cf..9ddf81f 100644 --- a/motm_app/requirements.txt +++ b/motm_app/requirements.txt @@ -9,6 +9,11 @@ wtforms>=3.0.0 wtforms_components MarkupSafe>=2.0.0 +# Web scraping +requests>=2.31.0 +beautifulsoup4>=4.12.0 +lxml>=4.9.0 + # SQLAlchemy and database drivers SQLAlchemy>=2.0.0 Flask-SQLAlchemy>=3.0.0 diff --git a/motm_app/templates/admin_dashboard.html b/motm_app/templates/admin_dashboard.html new file mode 100644 index 0000000..edbebb7 --- /dev/null +++ b/motm_app/templates/admin_dashboard.html @@ -0,0 +1,186 @@ + + + + + + Admin Dashboard - HKFC Men's C Team MOTM System + + + + + + + + + + diff --git a/motm_app/templates/club_management.html b/motm_app/templates/club_management.html index cb70602..bf0bfc4 100644 --- a/motm_app/templates/club_management.html +++ b/motm_app/templates/club_management.html @@ -15,9 +15,45 @@
Add New Club + + Back to Admin
+ + + + + + {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} @@ -79,5 +115,192 @@ + + diff --git a/motm_app/templates/error.html b/motm_app/templates/error.html index 44b2e6d..14eb924 100644 --- a/motm_app/templates/error.html +++ b/motm_app/templates/error.html @@ -10,7 +10,7 @@

Error

-

Invalid voting URL. Please check the link and try again.

+

{{ message or "Invalid voting URL. Please check the link and try again." }}

Home
diff --git a/motm_app/templates/match_squad.html b/motm_app/templates/match_squad.html index 905d317..9b572c3 100644 --- a/motm_app/templates/match_squad.html +++ b/motm_app/templates/match_squad.html @@ -41,11 +41,11 @@ {% for player in players %}
- -
diff --git a/motm_app/templates/match_squad_selected.html b/motm_app/templates/match_squad_selected.html index 9ce3fd7..94e03f7 100644 --- a/motm_app/templates/match_squad_selected.html +++ b/motm_app/templates/match_squad_selected.html @@ -8,17 +8,33 @@ -

Match Squad

+

Match Squad

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + + {{ table }}
- Add More Players - View Squad List - Reset Squad - Cancel diff --git a/motm_app/templates/motm_admin.html b/motm_app/templates/motm_admin.html index 9a63958..a24e346 100644 --- a/motm_app/templates/motm_admin.html +++ b/motm_app/templates/motm_admin.html @@ -8,6 +8,11 @@

HKFC Men's C Team MotM and DotD online vote admin page

+
+ + Back to Admin Dashboard + +
{% with messages = get_flashed_messages() %} {% if messages %} {% for message in messages %} @@ -28,11 +33,27 @@
+ +
+
+
+ + + View HK Hockey Fixtures + + + +
+
+
+
Date: - {{ form.nextMatchDate(class_="form-control") }} + {{ form.nextMatchDate(class_="form-control", id="nextMatchDate") }}
@@ -41,7 +62,7 @@
Opposition - {{ form.nextOppoTeam(class_="form-control") }} + {{ form.nextOppoTeam(class_="form-control", id="nextOppoTeam") }}
@@ -59,6 +80,15 @@
+ {% if not form.currMotM.choices or form.currMotM.choices|length == 0 %} +
+
+
+ Note: No players available for previous MOTM/DotD. This is normal if you haven't set up a match squad yet. You can still save the match details. +
+
+
+ {% endif %}

{{ form.saveButton(class_="btn btn-success") }} {{ form.activateButton(class_="btn btn-primary") }} @@ -74,8 +104,73 @@ diff --git a/motm_app/templates/player_management.html b/motm_app/templates/player_management.html index 2347a53..f4458ec 100644 --- a/motm_app/templates/player_management.html +++ b/motm_app/templates/player_management.html @@ -50,18 +50,18 @@ {% for player in players %} - {{ player.playerNumber }} - {{ player.playerForenames }} - {{ player.playerSurname }} - {{ player.playerNickname }} + {{ player.playernumber }} + {{ player.playerforenames }} + {{ player.playersurname }} + {{ player.playernickname }} - - {{ player.playerTeam }} + + {{ player.playerteam }} - Edit - + Edit +