gcp-hockey-results/motm_app/main.py

2558 lines
116 KiB
Python

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