Latest updates to use postgres

This commit is contained in:
Jonny Ervine 2025-10-01 23:25:05 +08:00
parent acb88ae785
commit 7f0d171e21
12 changed files with 1546 additions and 143 deletions

237
motm_app/club_scraper.py Normal file
View File

@ -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)

328
motm_app/fixture_scraper.py Normal file
View File

@ -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.")

View File

@ -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/<randomUrlSuffix>')
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/<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/<club>')
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)

View File

@ -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:

View File

@ -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

View File

@ -0,0 +1,186 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Dashboard - HKFC Men's C Team MOTM System</title>
<link rel="stylesheet" media="screen" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
<style>
.admin-section {
margin-bottom: 30px;
}
.section-header {
background-color: #f5f5f5;
padding: 15px;
border-left: 4px solid #337ab7;
margin-bottom: 15px;
}
.card-custom {
transition: transform 0.2s;
}
.card-custom:hover {
transform: translateY(-5px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="page-header">
<h1>HKFC Men's C Team - Admin Dashboard</h1>
<p class="lead">Central hub for all administrative functions</p>
</div>
<div class="mb-3">
<a href="/" class="btn btn-default">Back to Main Page</a>
</div>
<!-- Data Management Section -->
<div class="admin-section">
<div class="section-header">
<h3>Data Management</h3>
</div>
<div class="row">
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/players" class="list-group-item">
<h4 class="list-group-item-heading">Player Management</h4>
<p class="list-group-item-text">Add, edit, and manage players</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/clubs" class="list-group-item">
<h4 class="list-group-item-heading">Club Management</h4>
<p class="list-group-item-text">Manage hockey clubs</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/teams" class="list-group-item">
<h4 class="list-group-item-heading">Team Management</h4>
<p class="list-group-item-text">Manage hockey teams</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/import" class="list-group-item">
<h4 class="list-group-item-heading">Data Import</h4>
<p class="list-group-item-text">Import clubs and teams</p>
</a>
</div>
</div>
</div>
</div>
<!-- Match Management Section -->
<div class="admin-section">
<div class="section-header">
<h3>Match Management</h3>
</div>
<div class="row">
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/squad" class="list-group-item">
<h4 class="list-group-item-heading">Squad Selection</h4>
<p class="list-group-item-text">Select match squad</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/squad/list" class="list-group-item">
<h4 class="list-group-item-heading">View Squad</h4>
<p class="list-group-item-text">View current squad</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/squad/reset" class="list-group-item">
<h4 class="list-group-item-heading">Reset Squad</h4>
<p class="list-group-item-text">Reset for new match</p>
</a>
</div>
</div>
<div class="col-md-3">
<div class="list-group card-custom">
<a href="/admin/stats" class="list-group-item">
<h4 class="list-group-item-heading">Goals & Assists</h4>
<p class="list-group-item-text">Record statistics</p>
</a>
</div>
</div>
</div>
</div>
<!-- MOTM Management Section -->
<div class="admin-section">
<div class="section-header">
<h3>MOTM Management</h3>
</div>
<div class="row">
<div class="col-md-4">
<div class="list-group card-custom">
<a href="/admin/motm" class="list-group-item">
<h4 class="list-group-item-heading">MOTM Admin</h4>
<p class="list-group-item-text">Manage match settings and activate voting</p>
</a>
</div>
</div>
<div class="col-md-4">
<div class="list-group card-custom">
<a href="/admin/voting" class="list-group-item">
<h4 class="list-group-item-heading">Voting Results</h4>
<p class="list-group-item-text">View current match results</p>
</a>
</div>
</div>
<div class="col-md-4">
<div class="list-group card-custom">
<a href="/admin/poty" class="list-group-item">
<h4 class="list-group-item-heading">Player of the Year</h4>
<p class="list-group-item-text">View season standings</p>
</a>
</div>
</div>
</div>
</div>
<!-- System Management Section -->
<div class="admin-section">
<div class="section-header">
<h3>System Management</h3>
</div>
<div class="row">
<div class="col-md-6">
<div class="list-group card-custom">
<a href="/admin/database-setup" class="list-group-item">
<h4 class="list-group-item-heading">Database Setup</h4>
<p class="list-group-item-text">Configure and initialize database</p>
</a>
</div>
</div>
<div class="col-md-6">
<div class="list-group card-custom">
<a href="/admin/database-status" class="list-group-item">
<h4 class="list-group-item-heading">Database Status</h4>
<p class="list-group-item-text">View database configuration</p>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -15,9 +15,45 @@
<div class="mb-3">
<a href="/admin/clubs/add" class="btn btn-primary">Add New Club</a>
<button type="button" class="btn btn-info" id="importClubsBtn" onclick="importClubs()">
<span class="spinner-border spinner-border-sm d-none" id="importSpinner"></span>
Import from Hockey HK
</button>
<button type="button" class="btn btn-outline-info" id="previewClubsBtn" onclick="previewClubs()">
Preview Clubs
</button>
<a href="/admin" class="btn btn-secondary">Back to Admin</a>
</div>
<!-- Import Status -->
<div id="importStatus" class="alert d-none" role="alert"></div>
<!-- Preview Modal -->
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="previewModalLabel">Clubs from Hockey Hong Kong</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="previewContent">
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p>Loading clubs...</p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="confirmImportBtn" onclick="confirmImport()">Import All</button>
</div>
</div>
</div>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
@ -79,5 +115,192 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let previewClubs = [];
function showStatus(message, type = 'info') {
const statusDiv = document.getElementById('importStatus');
statusDiv.className = `alert alert-${type}`;
statusDiv.textContent = message;
statusDiv.classList.remove('d-none');
// Auto-hide after 5 seconds
setTimeout(() => {
statusDiv.classList.add('d-none');
}, 5000);
}
function previewClubs() {
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
const content = document.getElementById('previewContent');
// Show loading state
content.innerHTML = `
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p>Loading clubs from Hockey Hong Kong...</p>
</div>
`;
modal.show();
// Fetch clubs
fetch('/admin/api/clubs')
.then(response => response.json())
.then(data => {
if (data.success) {
previewClubs = data.clubs;
displayPreviewClubs(data.clubs);
} else {
content.innerHTML = `
<div class="alert alert-warning">
<h6>Unable to fetch clubs</h6>
<p>${data.message}</p>
<p><small>This might be due to website structure changes or network issues.</small></p>
</div>
`;
}
})
.catch(error => {
console.error('Error:', error);
content.innerHTML = `
<div class="alert alert-danger">
<h6>Error loading clubs</h6>
<p>There was an error fetching clubs from the Hockey Hong Kong website.</p>
</div>
`;
});
}
function displayPreviewClubs(clubs) {
const content = document.getElementById('previewContent');
if (clubs.length === 0) {
content.innerHTML = `
<div class="alert alert-info">
<h6>No clubs found</h6>
<p>The website structure may have changed or no clubs are currently listed.</p>
</div>
`;
return;
}
let html = `
<div class="alert alert-info">
<h6>Found ${clubs.length} clubs</h6>
<p>These clubs will be imported with their full names and abbreviations.</p>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Club Name</th>
<th>Abbreviation</th>
<th>Teams</th>
</tr>
</thead>
<tbody>
`;
clubs.forEach(club => {
const teams = club.teams ? club.teams.join(', ') : 'N/A';
html += `
<tr>
<td>${club.name}</td>
<td><span class="badge bg-secondary">${club.abbreviation || 'N/A'}</span></td>
<td>${teams}</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
content.innerHTML = html;
}
function confirmImport() {
const importBtn = document.getElementById('confirmImportBtn');
const spinner = document.getElementById('importSpinner');
// Show loading state
importBtn.disabled = true;
importBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Importing...';
// Import clubs
fetch('/admin/api/import-clubs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showStatus(data.message, 'success');
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('previewModal'));
modal.hide();
// Reload page to show updated clubs
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
showStatus(data.message, 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showStatus('Error importing clubs', 'danger');
})
.finally(() => {
importBtn.disabled = false;
importBtn.innerHTML = 'Import All';
});
}
function importClubs() {
const importBtn = document.getElementById('importClubsBtn');
const spinner = document.getElementById('importSpinner');
// Show loading state
importBtn.disabled = true;
spinner.classList.remove('d-none');
// Import clubs directly
fetch('/admin/api/import-clubs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showStatus(data.message, 'success');
// Reload page to show updated clubs
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
showStatus(data.message, 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showStatus('Error importing clubs', 'danger');
})
.finally(() => {
importBtn.disabled = false;
spinner.classList.add('d-none');
});
}
</script>
</body>
</html>

View File

@ -10,7 +10,7 @@
<div class="row">
<div class="col-md-12">
<h1>Error</h1>
<p>Invalid voting URL. Please check the link and try again.</p>
<p>{{ message or "Invalid voting URL. Please check the link and try again." }}</p>
<a class="btn btn-primary" href="/" role="button">Home</a>
</div>
</div>

View File

@ -41,11 +41,11 @@
{% for player in players %}
<div class="col-md-4 mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="playerNumber" value="{{ player.playerNumber }}" id="player{{ player.playerNumber }}">
<label class="form-check-label" for="player{{ player.playerNumber }}">
<strong>#{{ player.playerNumber }}</strong> {{ player.playerForenames }} {{ player.playerSurname }}
<input class="form-check-input" type="checkbox" name="playerNumber" value="{{ player.playernumber }}" id="player{{ player.playernumber }}">
<label class="form-check-label" for="player{{ player.playernumber }}">
<strong>#{{ player.playernumber }}</strong> {{ player.playerforenames }} {{ player.playersurname }}
<br>
<small class="text-muted">{{ player.playerNickname }} - {{ player.playerTeam }}</small>
<small class="text-muted">{{ player.playernickname }} - {{ player.playerteam }}</small>
</label>
</div>
</div>

View File

@ -8,17 +8,33 @@
<script src="/static/js/bootstrap.min.js"></script>
</head>
<body>
<h3>Match Squad</h3>
<div class="container">
<div class="row">
<div class="col-md-12">
<h3>Match Squad</h3>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'info' }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="mb-3">
<a class="btn btn-primary" href="/admin/squad" role="button">Add More Players</a>
<a class="btn btn-info" href="/admin/squad/list" role="button">View Squad List</a>
<a class="btn btn-warning" href="/admin/squad/reset" role="button">Reset Squad</a>
<a class="btn btn-secondary" href="/admin" role="button">Back to Admin</a>
<a class="btn btn-danger" href="/" role="button">Cancel</a>
</div>
{{ table }}
</div>
</div>
</div>
<a class="btn btn-primary" href="/admin/squad" role="button">Add More Players</a>
<a class="btn btn-info" href="/admin/squad/list" role="button">View Squad List</a>
<a class="btn btn-warning" href="/admin/squad/reset" role="button">Reset Squad</a>
<a class="btn btn-danger" href="/" role="button">Cancel</a>
</body>
</html>

View File

@ -8,6 +8,11 @@
<script src="/static/js/bootstrap.min.js"></script>
</head>
<h2>HKFC Men's C Team MotM and DotD online vote admin page</h2>
<div style="margin-bottom: 15px;">
<a href="/admin" class="btn btn-default btn-sm">
<span class="glyphicon glyphicon-arrow-left"></span> Back to Admin Dashboard
</a>
</div>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
@ -28,11 +33,27 @@
<div class="row">
<div class="col-xs-12">
<form class="col-sm-6" method="post" action="/admin/motm">
<!-- Load Next Fixture Button -->
<div class="row">
<div class="col-sm-12">
<div class="alert alert-info" style="margin-bottom: 15px;">
<button type="button" class="btn btn-info btn-sm" id="loadFixtureBtn" onclick="loadNextFixture()">
<span class="glyphicon glyphicon-download-alt"></span> Load Next HKFC C Fixture
</button>
<a href="https://hockey.org.hk/MenFixture.asp" target="_blank" class="btn btn-default btn-sm" style="margin-left: 5px;">
<span class="glyphicon glyphicon-new-window"></span> View HK Hockey Fixtures
</a>
<span id="fixtureStatus" style="margin-left: 10px;"></span>
<div id="fixtureInfo" style="margin-top: 10px; display: none;"></div>
</div>
</div>
</div>
<div class = "row">
<div class = "col-sm-6">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Date:</span>
{{ form.nextMatchDate(class_="form-control") }}
{{ form.nextMatchDate(class_="form-control", id="nextMatchDate") }}
</div>
</div>
</div>
@ -41,7 +62,7 @@
<div class = "col-sm-9">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Opposition</span>
{{ form.nextOppoTeam(class_="form-control") }}
{{ form.nextOppoTeam(class_="form-control", id="nextOppoTeam") }}
</div>
</div>
</div>
@ -59,6 +80,15 @@
</div>
</div>
</div>
{% if not form.currMotM.choices or form.currMotM.choices|length == 0 %}
<div class="row">
<div class="col-sm-12">
<div class="alert alert-warning" style="margin-top: 10px;">
<small><strong>Note:</strong> No players available for previous MOTM/DotD. This is normal if you haven't set up a match squad yet. You can still save the match details.</small>
</div>
</div>
</div>
{% endif %}
<p>
{{ form.saveButton(class_="btn btn-success") }}
{{ form.activateButton(class_="btn btn-primary") }}
@ -74,8 +104,73 @@
</dl>
<script>
// Date input functionality - no longer needed to fetch fixture data
// The date input will be handled by the browser's native date picker
function loadNextFixture() {
// Show loading status
var statusElement = document.getElementById('fixtureStatus');
var infoElement = document.getElementById('fixtureInfo');
var loadBtn = document.getElementById('loadFixtureBtn');
statusElement.innerHTML = '<span class="text-info">Loading...</span>';
loadBtn.disabled = true;
// Fetch the next fixture from the API
fetch('/admin/api/next-fixture')
.then(response => response.json())
.then(data => {
if (data.success) {
// Update the form fields
document.getElementById('nextMatchDate').value = data.date;
document.getElementById('nextOppoTeam').value = data.opponent;
// Show fixture information
let clubInfo = '';
if (data.opponent_club_info) {
const club = data.opponent_club_info;
const confidence = club.match_result ? club.match_result.confidence : 'unknown';
const matchType = club.match_result ? club.match_result.match_type : 'unknown';
clubInfo = '<br><small class="text-muted">';
clubInfo += 'Club: ' + club.club_name;
if (club.logo_url) {
clubInfo += ' | <a href="' + club.logo_url + '" target="_blank">Logo</a>';
}
clubInfo += ' | Match: ' + matchType + ' (' + confidence + ')';
clubInfo += '</small>';
}
infoElement.innerHTML = '<strong>Next Match:</strong> ' +
data.date_formatted + ' vs ' + data.opponent +
' (' + (data.is_home ? 'Home' : 'Away') + ' - ' + data.venue + ')' +
'<br><small>Division: ' + data.division + ' | Time: ' + data.time + '</small>' +
clubInfo;
infoElement.style.display = 'block';
statusElement.innerHTML = '<span class="text-success">✓ Fixture loaded!</span>';
// Clear status message after 3 seconds
setTimeout(function() {
statusElement.innerHTML = '';
}, 3000);
} else {
statusElement.innerHTML = '<span class="text-danger">✗ ' + data.message + '</span>';
infoElement.style.display = 'none';
}
loadBtn.disabled = false;
})
.catch(error => {
console.error('Error:', error);
statusElement.innerHTML = '<span class="text-danger">✗ Error loading fixture</span>';
infoElement.style.display = 'none';
loadBtn.disabled = false;
});
}
// Auto-load fixture on page load
function myFunction() {
// Optional: Auto-load the next fixture when the page loads
// Uncomment the next line if you want this behavior
// loadNextFixture();
}
</script>
</body>
</html>

View File

@ -50,18 +50,18 @@
<tbody>
{% for player in players %}
<tr>
<td>{{ player.playerNumber }}</td>
<td>{{ player.playerForenames }}</td>
<td>{{ player.playerSurname }}</td>
<td>{{ player.playerNickname }}</td>
<td>{{ player.playernumber }}</td>
<td>{{ player.playerforenames }}</td>
<td>{{ player.playersurname }}</td>
<td>{{ player.playernickname }}</td>
<td>
<span class="badge bg-{{ 'primary' if player.playerTeam == 'HKFC C' else 'secondary' }}">
{{ player.playerTeam }}
<span class="badge bg-{{ 'primary' if player.playerteam == 'HKFC C' else 'secondary' }}">
{{ player.playerteam }}
</span>
</td>
<td>
<a href="/admin/players/edit/{{ player.playerNumber }}" class="btn btn-sm btn-outline-primary">Edit</a>
<form method="POST" action="/admin/players/delete/{{ player.playerNumber }}" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this player?')">
<a href="/admin/players/edit/{{ player.playernumber }}" class="btn btn-sm btn-outline-primary">Edit</a>
<form method="POST" action="/admin/players/delete/{{ player.playernumber }}" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this player?')">
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
</form>
</td>