2040 lines
92 KiB
Python
2040 lines
92 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
|
|
from sqlalchemy import text
|
|
from flask_wtf import FlaskForm
|
|
from flask_bootstrap import Bootstrap
|
|
from flask_basicauth import BasicAuth
|
|
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
|
from wtforms import DateField
|
|
from wtforms.validators import InputRequired, Email, Length, EqualTo
|
|
from forms import motmForm, adminSettingsForm2, goalsAssistsForm, DatabaseSetupForm, PlayerForm, ClubForm, TeamForm, DataImportForm, ClubSelectionForm, S3ConfigForm
|
|
from db_config import sql_write, sql_write_static, sql_read, sql_read_static
|
|
from sqlalchemy import text
|
|
from tables import matchSquadTable
|
|
from readSettings import mySettings
|
|
from fixture_scraper import FixtureScraper, get_next_hkfc_c_fixture, get_opponent_club_name, get_opponent_club_info, match_opponent_to_club
|
|
from club_scraper import ClubScraper, get_hk_hockey_clubs, expand_club_abbreviation
|
|
from s3_config import s3_config_manager, s3_asset_service
|
|
|
|
# Custom authentication class that uses database
|
|
class DatabaseBasicAuth(BasicAuth):
|
|
def check_credentials(self, username, password):
|
|
try:
|
|
sql = text("SELECT password_hash FROM admin_profiles WHERE username = :username")
|
|
result = sql_read(sql, {'username': username})
|
|
if result:
|
|
stored_hash = result[0]['password_hash']
|
|
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
|
return password_hash == stored_hash
|
|
return False
|
|
except Exception as e:
|
|
print(f"Authentication error: {e}")
|
|
return False
|
|
|
|
app.config['BASIC_AUTH_USERNAME'] = 'admin' # Fallback for compatibility
|
|
app.config['BASIC_AUTH_PASSWORD'] = 'letmein' # Fallback for compatibility
|
|
basic_auth = DatabaseBasicAuth(app)
|
|
|
|
# Admin profile form
|
|
class AdminProfileForm(FlaskForm):
|
|
current_password = PasswordField('Current Password', validators=[InputRequired()])
|
|
new_password = PasswordField('New Password', validators=[InputRequired(), Length(min=6)])
|
|
confirm_password = PasswordField('Confirm New Password', validators=[InputRequired(), EqualTo('new_password', message='Passwords must match')])
|
|
email = StringField('Email', validators=[Email()])
|
|
submit = SubmitField('Update Profile')
|
|
|
|
|
|
@app.route('/')
|
|
def index():
|
|
"""Main index page for MOTM system"""
|
|
# Check if user is authenticated as admin
|
|
is_admin = is_admin_authenticated(request)
|
|
return render_template('index.html', is_admin=is_admin)
|
|
|
|
|
|
@app.route('/admin')
|
|
@basic_auth.required
|
|
def admin_dashboard():
|
|
"""Admin dashboard - central hub for all admin functions"""
|
|
return render_template('admin_dashboard.html')
|
|
|
|
|
|
@app.route('/admin/profile', methods=['GET', 'POST'])
|
|
@basic_auth.required
|
|
def admin_profile():
|
|
"""Admin profile page for changing password and settings"""
|
|
form = AdminProfileForm()
|
|
|
|
# Get current admin profile
|
|
sql = text("SELECT username, email FROM admin_profiles WHERE username = 'admin'")
|
|
profile = sql_read(sql)
|
|
|
|
if profile:
|
|
current_email = profile[0]['email']
|
|
else:
|
|
current_email = ''
|
|
|
|
if request.method == 'POST' and form.validate_on_submit():
|
|
# Verify current password
|
|
sql_check = text("SELECT password_hash FROM admin_profiles WHERE username = 'admin'")
|
|
result = sql_read(sql_check)
|
|
|
|
if result:
|
|
stored_hash = result[0]['password_hash']
|
|
current_password_hash = hashlib.sha256(form.current_password.data.encode()).hexdigest()
|
|
|
|
if current_password_hash == stored_hash:
|
|
# Update password and email
|
|
new_password_hash = hashlib.sha256(form.new_password.data.encode()).hexdigest()
|
|
|
|
sql_update = text("""
|
|
UPDATE admin_profiles
|
|
SET password_hash = :password_hash, email = :email, updated_at = CURRENT_TIMESTAMP
|
|
WHERE username = 'admin'
|
|
""")
|
|
sql_write(sql_update, {
|
|
'password_hash': new_password_hash,
|
|
'email': form.email.data
|
|
})
|
|
|
|
flash('Profile updated successfully!', 'success')
|
|
return redirect(url_for('admin_profile'))
|
|
else:
|
|
flash('Current password is incorrect!', 'error')
|
|
else:
|
|
flash('Admin profile not found!', 'error')
|
|
|
|
# Pre-populate email field
|
|
form.email.data = current_email
|
|
|
|
return render_template('admin_profile.html', form=form, current_email=current_email)
|
|
|
|
|
|
def generate_device_id(request):
|
|
"""Generate a device identifier from request headers"""
|
|
import hashlib
|
|
|
|
# Collect device characteristics
|
|
user_agent = request.headers.get('User-Agent', '')
|
|
accept_language = request.headers.get('Accept-Language', '')
|
|
accept_encoding = request.headers.get('Accept-Encoding', '')
|
|
ip_address = request.environ.get('REMOTE_ADDR', '')
|
|
|
|
# Create a fingerprint from these characteristics
|
|
fingerprint_string = f"{user_agent}|{accept_language}|{accept_encoding}|{ip_address}"
|
|
device_id = hashlib.sha256(fingerprint_string.encode()).hexdigest()[:16] # Use first 16 chars
|
|
|
|
return device_id
|
|
|
|
|
|
def is_admin_authenticated(request):
|
|
"""Check if the current request is authenticated as admin"""
|
|
try:
|
|
# Check if Authorization header exists
|
|
auth_header = request.headers.get('Authorization', '')
|
|
if not auth_header.startswith('Basic '):
|
|
return False
|
|
|
|
# Decode the basic auth credentials
|
|
import base64
|
|
encoded_credentials = auth_header[6:] # Remove 'Basic ' prefix
|
|
try:
|
|
credentials = base64.b64decode(encoded_credentials).decode('utf-8')
|
|
username, password = credentials.split(':', 1)
|
|
|
|
# Check against database
|
|
sql = text("SELECT password_hash FROM admin_profiles WHERE username = :username")
|
|
result = sql_read(sql, {'username': username})
|
|
|
|
if result:
|
|
stored_hash = result[0]['password_hash']
|
|
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
|
return password_hash == stored_hash
|
|
except:
|
|
pass
|
|
except:
|
|
pass
|
|
|
|
return False
|
|
|
|
|
|
# ==================== 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 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/hkfc_logo.png') # Default fallback
|
|
sql_hkfc_logo = text("SELECT logo_url FROM clubs WHERE hockey_club = 'Hong Kong Football Club'")
|
|
hkfc_logo_result = sql_read(sql_hkfc_logo)
|
|
if hkfc_logo_result and hkfc_logo_result[0]['logo_url']:
|
|
hkfcLogo = s3_asset_service.get_logo_url(hkfc_logo_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)
|
|
|
|
currMotM = nextInfo[0]['currmotm']
|
|
currDotD = nextInfo[0]['currdotd']
|
|
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 pictures
|
|
if nextInfo and nextInfo[0]['currmotm'] and nextInfo[0]['currdotd']:
|
|
sql3 = text("SELECT playernickname FROM _hkfc_players WHERE playernumber = :curr_motm")
|
|
sql4 = text("SELECT playernickname FROM _hkfc_players WHERE playernumber = :curr_dotd")
|
|
motm = sql_read(sql3, {'curr_motm': nextInfo[0]['currmotm']})
|
|
dotd = sql_read(sql4, {'curr_dotd': nextInfo[0]['currdotd']})
|
|
else:
|
|
motm = []
|
|
dotd = []
|
|
|
|
# Handle empty results
|
|
if not motm or not dotd:
|
|
return render_template('error.html', message="Player data not found. Please set up current MOTM and DotD players in admin settings.")
|
|
|
|
# Use default player images since playerPictureURL column doesn't exist
|
|
motmURL = 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()
|
|
|
|
# Verify URL suffix
|
|
if nextInfo and nextInfo[0].get('motmurlsuffix'):
|
|
randomSuff = nextInfo[0]['motmurlsuffix']
|
|
if randomSuff == randomUrlSuffix:
|
|
# Use nextdate to generate proper match number instead of nextfixture
|
|
match_number = nextInfo[0]['nextdate'].strftime('%Y-%m-%d') if nextInfo[0]['nextdate'] else ''
|
|
return render_template('motm_vote.html', data=rows, comment=comment, formatDate=formatDate, matchNumber=match_number, oppo=oppo, hkfcLogo=hkfcLogo, oppoLogo=oppoLogo, dotdURL=dotdURL, motmURL=motmURL, form=form)
|
|
else:
|
|
return render_template('error.html', message="Invalid voting URL. Please use the correct URL provided by the admin.")
|
|
else:
|
|
return render_template('error.html', message="Voting not activated. Please contact the admin to activate voting.")
|
|
|
|
|
|
@app.route('/motm/comments', methods=['GET', 'POST'])
|
|
def match_comments():
|
|
"""Display and allow adding match comments"""
|
|
sql = text("SELECT nextclub, nextteam, nextdate, oppologo, hkfclogo FROM motmadminsettings")
|
|
row = sql_read_static(sql)
|
|
if not row:
|
|
return render_template('error.html', message="Database not initialized. Please go to Database Setup to initialize the database.")
|
|
|
|
_oppo = row[0]['nextclub']
|
|
commentDate = row[0]['nextdate'].strftime('%Y-%m-%d')
|
|
_matchDate = row[0]['nextdate'].strftime('%Y_%m_%d')
|
|
|
|
# Get HKFC logo from clubs table using signed URLs (with authentication)
|
|
hkfcLogo = s3_asset_service.get_asset_url('images/hkfc_logo.png') # Default fallback
|
|
sql_hkfc_logo = text("SELECT logo_url FROM clubs WHERE hockey_club = 'Hong Kong Football Club'")
|
|
hkfc_logo_result = sql_read(sql_hkfc_logo)
|
|
if hkfc_logo_result and hkfc_logo_result[0]['logo_url']:
|
|
hkfcLogo = s3_asset_service.get_logo_url(hkfc_logo_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:
|
|
_motm = request.form['motmVote']
|
|
_dotd = request.form['dotdVote']
|
|
_comments = request.form['motmComment']
|
|
_fixed_comments = _comments.replace("'", "\\'")
|
|
_matchDate = request.form['matchNumber']
|
|
_oppo = request.form['oppo']
|
|
|
|
if _motm and _dotd and request.method == 'POST':
|
|
# Check if fixture columns exist, create them if not
|
|
fixture_date = _matchDate.replace('-', '')
|
|
motm_col = f'motm_{fixture_date}'
|
|
dotd_col = f'dotd_{fixture_date}'
|
|
|
|
# Create columns if they don't exist
|
|
try:
|
|
sql_create_motm = text(f"ALTER TABLE _hkfc_c_motm ADD COLUMN {motm_col} INTEGER DEFAULT 0")
|
|
sql_write(sql_create_motm)
|
|
except:
|
|
pass # Column already exists
|
|
|
|
try:
|
|
sql_create_dotd = text(f"ALTER TABLE _hkfc_c_motm ADD COLUMN {dotd_col} INTEGER DEFAULT 0")
|
|
sql_write(sql_create_dotd)
|
|
except:
|
|
pass # Column already exists
|
|
|
|
# Get player names
|
|
motm_player = sql_read(text("SELECT playernickname FROM _hkfc_players WHERE playernumber = :player_num"), {'player_num': _motm})
|
|
dotd_player = sql_read(text("SELECT playernickname FROM _hkfc_players WHERE playernumber = :player_num"), {'player_num': _dotd})
|
|
|
|
motm_name = motm_player[0]['playernickname'] if motm_player else f'Player {_motm}'
|
|
dotd_name = dotd_player[0]['playernickname'] if dotd_player else f'Player {_dotd}'
|
|
|
|
# Update MOTM vote - use PostgreSQL UPSERT syntax (don't update totals)
|
|
sql_motm = text(f"""
|
|
INSERT INTO _hkfc_c_motm (playernumber, playername, {motm_col})
|
|
VALUES (:player_num, :player_name, 1)
|
|
ON CONFLICT (playernumber) DO UPDATE SET
|
|
{motm_col} = _hkfc_c_motm.{motm_col} + 1
|
|
""")
|
|
sql_write(sql_motm, {'player_num': _motm, 'player_name': motm_name})
|
|
|
|
# Update DotD vote - use PostgreSQL UPSERT syntax (don't update totals)
|
|
sql_dotd = text(f"""
|
|
INSERT INTO _hkfc_c_motm (playernumber, playername, {dotd_col})
|
|
VALUES (:player_num, :player_name, 1)
|
|
ON CONFLICT (playernumber) DO UPDATE SET
|
|
{dotd_col} = _hkfc_c_motm.{dotd_col} + 1
|
|
""")
|
|
sql_write(sql_dotd, {'player_num': _dotd, 'player_name': dotd_name})
|
|
|
|
# Generate device identifier and record vote for tracking
|
|
device_id = generate_device_id(request)
|
|
sql_device = text("""
|
|
INSERT INTO device_votes (device_id, fixture_date, motm_player_number, dotd_player_number,
|
|
motm_player_name, dotd_player_name, ip_address, user_agent)
|
|
VALUES (:device_id, :fixture_date, :motm_player, :dotd_player,
|
|
:motm_name, :dotd_name, :ip_address, :user_agent)
|
|
""")
|
|
sql_write(sql_device, {
|
|
'device_id': device_id,
|
|
'fixture_date': fixture_date,
|
|
'motm_player': _motm,
|
|
'dotd_player': _dotd,
|
|
'motm_name': motm_name,
|
|
'dotd_name': dotd_name,
|
|
'ip_address': request.environ.get('REMOTE_ADDR', 'unknown'),
|
|
'user_agent': request.headers.get('User-Agent', 'unknown')
|
|
})
|
|
|
|
# Handle comments
|
|
if _comments and _comments != "Optional comments added here":
|
|
sql3 = text("INSERT INTO _motmcomments (matchDate, comment) VALUES (:match_date, :comment)")
|
|
sql_write(sql3, {'match_date': _matchDate, 'comment': _fixed_comments})
|
|
|
|
return render_template('vote_thanks.html')
|
|
else:
|
|
return 'Ouch ... something went wrong here'
|
|
except Exception as e:
|
|
print(e)
|
|
finally:
|
|
print('Votes cast')
|
|
|
|
|
|
# ==================== ADMIN SECTION ====================
|
|
|
|
@app.route('/admin/motm', methods=['GET', 'POST'])
|
|
@basic_auth.required
|
|
def motm_admin():
|
|
"""Admin page for managing MOTM settings"""
|
|
form = adminSettingsForm2()
|
|
prevFixture = mySettings('prevFixture')
|
|
if prevFixture is None:
|
|
prevFixture = '1'
|
|
else:
|
|
prevFixture = str(prevFixture)
|
|
if request.method == 'POST':
|
|
if form.saveButton.data:
|
|
print('Saved')
|
|
else:
|
|
print('Activated')
|
|
_nextTeam = request.form.get('nextOppoTeam', '')
|
|
_nextMatchDate = request.form.get('nextMatchDate', '')
|
|
_currMotM = request.form.get('currMotM', '0')
|
|
_currDotD = request.form.get('currDotD', '0')
|
|
|
|
# Validate required fields
|
|
if not _nextTeam or not _nextMatchDate:
|
|
flash('Error: Match date and opposition team are required', 'error')
|
|
return redirect(url_for('motm_admin'))
|
|
|
|
# Use the opponent matching system to find the correct club
|
|
opponent_club_info = get_opponent_club_info(_nextTeam)
|
|
if opponent_club_info and opponent_club_info.get('club_name'):
|
|
_nextClub = opponent_club_info['club_name']
|
|
else:
|
|
# Fallback to old method for backward compatibility
|
|
sql1 = "SELECT club FROM _clubTeams WHERE displayName='" + _nextTeam + "'"
|
|
_nextClubName = sql_read_static(sql1)
|
|
if not _nextClubName:
|
|
flash('Error: Club not found for team ' + _nextTeam + '. Please ensure the club exists in the club database.', 'error')
|
|
return redirect(url_for('motm_admin'))
|
|
_nextClub = _nextClubName[0]['club']
|
|
|
|
# Only update currMotM and currDotD if they were provided
|
|
if _currMotM and _currMotM != '0' and _currDotD and _currDotD != '0':
|
|
sql = text("UPDATE motmadminsettings SET nextdate = :next_date, nextclub = :next_club, nextteam = :next_team, currmotm = :curr_motm, currdotd = :curr_dotd")
|
|
sql_write_static(sql, {
|
|
'next_date': _nextMatchDate,
|
|
'next_club': _nextClub,
|
|
'next_team': _nextTeam,
|
|
'curr_motm': _currMotM,
|
|
'curr_dotd': _currDotD
|
|
})
|
|
else:
|
|
# Don't update currMotM and currDotD if not provided
|
|
sql = text("UPDATE motmadminsettings SET nextdate = :next_date, nextclub = :next_club, nextteam = :next_team")
|
|
sql_write_static(sql, {
|
|
'next_date': _nextMatchDate,
|
|
'next_club': _nextClub,
|
|
'next_team': _nextTeam
|
|
})
|
|
|
|
# Update the opponent logo using the matched club information
|
|
if opponent_club_info and opponent_club_info.get('logo_url'):
|
|
# Use the logo URL from the matched club
|
|
logo_url = opponent_club_info['logo_url']
|
|
sql2 = text("UPDATE motmadminsettings SET oppologo = :logo_url")
|
|
sql_write_static(sql2, {'logo_url': logo_url})
|
|
else:
|
|
# Fallback to old method
|
|
sql2 = text("UPDATE motmadminsettings SET oppologo = (SELECT logo FROM menshockeyclubs WHERE hockeyclub = :next_club) WHERE nextclub = :next_club")
|
|
sql_write_static(sql2, {'next_club': _nextClub})
|
|
if form.saveButton.data:
|
|
flash('Settings saved!')
|
|
urlSuffix = randomUrlSuffix(8)
|
|
print(urlSuffix)
|
|
sql3 = text("UPDATE motmadminsettings SET motmurlsuffix = :url_suffix WHERE userid = 'admin'")
|
|
sql_write_static(sql3, {'url_suffix': urlSuffix})
|
|
flash('MotM URL https://motm.ervine.cloud/motm/'+urlSuffix)
|
|
elif form.activateButton.data:
|
|
# Generate a fixture number based on the date
|
|
_nextFixture = _nextMatchDate.replace('-', '')
|
|
try:
|
|
sql4 = text(f"ALTER TABLE _hkfc_c_motm ADD COLUMN motm_{_nextFixture} smallint DEFAULT 0, ADD COLUMN dotd_{_nextFixture} smallint DEFAULT 0, ADD COLUMN assists_{_nextFixture} smallint DEFAULT 0, ADD COLUMN goals_{_nextFixture} smallint DEFAULT 0")
|
|
sql_write(sql4)
|
|
except Exception as e:
|
|
# Columns already exist, which is fine
|
|
print(f"Columns already exist for fixture {_nextFixture}: {e}")
|
|
pass
|
|
sql5 = text("SELECT motmurlsuffix FROM motmadminsettings WHERE userid = 'admin'")
|
|
tempSuffix = sql_read_static(sql5)
|
|
if not tempSuffix:
|
|
flash('Error: Admin settings not found', 'error')
|
|
return redirect(url_for('motm_admin'))
|
|
currSuffix = tempSuffix[0]['motmurlsuffix']
|
|
print(currSuffix)
|
|
flash('Man of the Match vote is now activated')
|
|
flash('MotM URL https://motm.ervine.cloud/motm/'+currSuffix)
|
|
else:
|
|
flash('Something went wrong - check with Smithers')
|
|
|
|
# Load current settings to populate the form
|
|
sql_current = text("SELECT nextdate FROM motmadminsettings WHERE userid = 'admin'")
|
|
current_settings = sql_read_static(sql_current)
|
|
if current_settings:
|
|
from datetime import datetime
|
|
try:
|
|
current_date = current_settings[0]['nextdate']
|
|
if isinstance(current_date, str):
|
|
current_date = datetime.strptime(current_date, '%Y-%m-%d').date()
|
|
form.nextMatchDate.data = current_date
|
|
except:
|
|
pass
|
|
sql4 = text("SELECT hockeyclub FROM menshockeyclubs ORDER BY hockeyclub")
|
|
sql5 = text("SELECT nextclub, oppologo FROM motmadminsettings")
|
|
sql6 = text(f"SELECT playernumber, playerforenames, playersurname FROM _hkfcc_matchsquad_{prevFixture} ORDER BY playerforenames")
|
|
clubs = sql_read_static(sql4)
|
|
settings = sql_read_static(sql5)
|
|
players = sql_read(sql6)
|
|
|
|
# Handle empty results gracefully
|
|
if not clubs:
|
|
clubs = []
|
|
if not settings:
|
|
settings = [{'nextclub': 'Unknown', 'oppologo': '/static/images/default_logo.png'}]
|
|
if not players:
|
|
players = []
|
|
|
|
form.nextOppoClub.choices = [(oppo['hockeyclub'], oppo['hockeyclub']) for oppo in clubs]
|
|
form.currMotM.choices = [(player['playernumber'], player['playerforenames'] + " " + player['playersurname']) for player in players]
|
|
form.currDotD.choices = [(player['playernumber'], player['playerforenames'] + " " + player['playersurname']) for player in players]
|
|
clubLogo = settings[0]['oppologo']
|
|
|
|
return render_template('motm_admin.html', form=form, nextOppoLogo=clubLogo)
|
|
|
|
|
|
@app.route('/admin/players', methods=['GET'])
|
|
@basic_auth.required
|
|
def player_management():
|
|
"""Admin page for managing players"""
|
|
sql = text("SELECT playernumber, playerforenames, playersurname, playernickname, playerteam FROM _hkfc_players ORDER BY playernumber")
|
|
players = sql_read(sql)
|
|
return render_template('player_management.html', players=players)
|
|
|
|
|
|
@app.route('/admin/players/add', methods=['GET', 'POST'])
|
|
@basic_auth.required
|
|
def add_player():
|
|
"""Add a new player"""
|
|
form = PlayerForm()
|
|
|
|
if form.validate_on_submit():
|
|
if form.save_player.data:
|
|
# Check if player number already exists
|
|
sql_check = text("SELECT playernumber FROM _hkfc_players WHERE playernumber = :player_number")
|
|
existing = sql_read(sql_check, {'player_number': form.player_number.data})
|
|
|
|
if existing:
|
|
flash('Player number already exists!', 'error')
|
|
return render_template('add_player.html', form=form)
|
|
|
|
# Insert new player
|
|
sql = text("INSERT INTO _hkfc_players (playernumber, playerforenames, playersurname, playernickname, playerteam) VALUES (:player_number, :forenames, :surname, :nickname, :team)")
|
|
sql_write(sql, {
|
|
'player_number': form.player_number.data,
|
|
'forenames': form.player_forenames.data,
|
|
'surname': form.player_surname.data,
|
|
'nickname': form.player_nickname.data,
|
|
'team': form.player_team.data
|
|
})
|
|
flash('Player added successfully!', 'success')
|
|
return redirect(url_for('player_management'))
|
|
elif form.cancel.data:
|
|
return redirect(url_for('player_management'))
|
|
|
|
return render_template('add_player.html', form=form)
|
|
|
|
|
|
@app.route('/admin/players/edit/<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/hkfc_logo.png'},
|
|
{'hockey_club': 'KCC', 'logo_url': '/static/images/kcc_logo.png'},
|
|
{'hockey_club': 'USRC', 'logo_url': '/static/images/usrc_logo.png'},
|
|
{'hockey_club': 'Valley', 'logo_url': '/static/images/valley_logo.png'},
|
|
{'hockey_club': 'SSSC', 'logo_url': '/static/images/sssc_logo.png'},
|
|
{'hockey_club': 'Dragons', 'logo_url': '/static/images/dragons_logo.png'},
|
|
{'hockey_club': 'Kai Tak', 'logo_url': '/static/images/kaitak_logo.png'},
|
|
{'hockey_club': 'RHOBA', 'logo_url': '/static/images/rhoba_logo.png'},
|
|
{'hockey_club': 'Elite', 'logo_url': '/static/images/elite_logo.png'},
|
|
{'hockey_club': 'Aquila', 'logo_url': '/static/images/aquila_logo.png'},
|
|
{'hockey_club': 'HKJ', 'logo_url': '/static/images/hkj_logo.png'},
|
|
{'hockey_club': 'Sirius', 'logo_url': '/static/images/sirius_logo.png'},
|
|
{'hockey_club': 'Shaheen', 'logo_url': '/static/images/shaheen_logo.png'},
|
|
{'hockey_club': 'Diocesan', 'logo_url': '/static/images/diocesan_logo.png'},
|
|
{'hockey_club': 'Rhino', 'logo_url': '/static/images/rhino_logo.png'},
|
|
{'hockey_club': 'Khalsa', 'logo_url': '/static/images/khalsa_logo.png'},
|
|
{'hockey_club': 'HKCC', 'logo_url': '/static/images/hkcc_logo.png'},
|
|
{'hockey_club': 'Police', 'logo_url': '/static/images/police_logo.png'},
|
|
{'hockey_club': 'Recreio', 'logo_url': '/static/images/recreio_logo.png'},
|
|
{'hockey_club': 'CSD', 'logo_url': '/static/images/csd_logo.png'},
|
|
{'hockey_club': 'Dutch', 'logo_url': '/static/images/dutch_logo.png'},
|
|
{'hockey_club': 'HKUHC', 'logo_url': '/static/images/hkuhc_logo.png'},
|
|
{'hockey_club': 'Kaitiaki', 'logo_url': '/static/images/kaitiaki_logo.png'},
|
|
{'hockey_club': 'Antlers', 'logo_url': '/static/images/antlers_logo.png'},
|
|
{'hockey_club': 'Marcellin', 'logo_url': '/static/images/marcellin_logo.png'},
|
|
{'hockey_club': 'Skyers', 'logo_url': '/static/images/skyers_logo.png'},
|
|
{'hockey_club': 'JR', 'logo_url': '/static/images/jr_logo.png'},
|
|
{'hockey_club': 'IUHK', 'logo_url': '/static/images/iuhk_logo.png'},
|
|
{'hockey_club': '144U', 'logo_url': '/static/images/144u_logo.png'},
|
|
{'hockey_club': 'HKU', 'logo_url': '/static/images/hku_logo.png'},
|
|
]
|
|
|
|
for club_data in clubs_data:
|
|
# Check if club already exists
|
|
sql_check = text("SELECT hockey_club FROM clubs WHERE hockey_club = :club_name")
|
|
existing = sql_read(sql_check, {'club_name': club_data['hockey_club']})
|
|
|
|
if not existing:
|
|
sql = text("INSERT INTO clubs (hockey_club, logo_url) VALUES (:club_name, :logo_url)")
|
|
sql_write(sql, club_data)
|
|
imported_clubs += 1
|
|
|
|
if form.import_teams.data:
|
|
# Import teams based on Hong Kong Hockey Association divisions
|
|
teams_data = [
|
|
# Premier Division
|
|
{'club': 'HKFC', 'team': 'A', 'display_name': 'HKFC A', 'league': 'Premier Division'},
|
|
{'club': 'KCC', 'team': 'A', 'display_name': 'KCC A', 'league': 'Premier Division'},
|
|
{'club': 'USRC', 'team': 'A', 'display_name': 'USRC A', 'league': 'Premier Division'},
|
|
{'club': 'Valley', 'team': 'A', 'display_name': 'Valley A', 'league': 'Premier Division'},
|
|
|
|
# 1st Division
|
|
{'club': 'HKFC', 'team': 'B', 'display_name': 'HKFC B', 'league': '1st Division'},
|
|
{'club': 'KCC', 'team': 'B', 'display_name': 'KCC B', 'league': '1st Division'},
|
|
{'club': 'USRC', 'team': 'B', 'display_name': 'USRC B', 'league': '1st Division'},
|
|
{'club': 'Valley', 'team': 'B', 'display_name': 'Valley B', 'league': '1st Division'},
|
|
|
|
# 2nd Division
|
|
{'club': 'HKFC', 'team': 'C', 'display_name': 'HKFC C', 'league': '2nd Division'},
|
|
{'club': 'KCC', 'team': 'C', 'display_name': 'KCC C', 'league': '2nd Division'},
|
|
{'club': 'USRC', 'team': 'C', 'display_name': 'USRC C', 'league': '2nd Division'},
|
|
{'club': 'Valley', 'team': 'C', 'display_name': 'Valley C', 'league': '2nd Division'},
|
|
|
|
# 3rd Division
|
|
{'club': 'SSSC', 'team': 'C', 'display_name': 'SSSC C', 'league': '3rd Division'},
|
|
{'club': 'Dragons', 'team': 'A', 'display_name': 'Dragons A', 'league': '3rd Division'},
|
|
{'club': 'Kai Tak', 'team': 'B', 'display_name': 'Kai Tak B', 'league': '3rd Division'},
|
|
{'club': 'RHOBA', 'team': 'A', 'display_name': 'RHOBA A', 'league': '3rd Division'},
|
|
{'club': 'Elite', 'team': 'B', 'display_name': 'Elite B', 'league': '3rd Division'},
|
|
{'club': 'HKFC', 'team': 'F', 'display_name': 'HKFC F', 'league': '3rd Division'},
|
|
{'club': 'Aquila', 'team': 'A', 'display_name': 'Aquila A', 'league': '3rd Division'},
|
|
{'club': 'HKJ', 'team': 'B', 'display_name': 'HKJ B', 'league': '3rd Division'},
|
|
{'club': 'Sirius', 'team': 'A', 'display_name': 'Sirius A', 'league': '3rd Division'},
|
|
{'club': 'Shaheen', 'team': 'B', 'display_name': 'Shaheen B', 'league': '3rd Division'},
|
|
{'club': 'RHOBA', 'team': 'B', 'display_name': 'RHOBA B', 'league': '3rd Division'},
|
|
|
|
# 4th Division
|
|
{'club': 'Khalsa', 'team': 'C', 'display_name': 'Khalsa C', 'league': '4th Division'},
|
|
{'club': 'HKCC', 'team': 'C', 'display_name': 'HKCC C', 'league': '4th Division'},
|
|
{'club': 'Valley', 'team': 'D', 'display_name': 'Valley D', 'league': '4th Division'},
|
|
{'club': 'Police', 'team': 'A', 'display_name': 'Police A', 'league': '4th Division'},
|
|
{'club': 'Recreio', 'team': 'A', 'display_name': 'Recreio A', 'league': '4th Division'},
|
|
{'club': 'CSD', 'team': 'A', 'display_name': 'CSD A', 'league': '4th Division'},
|
|
{'club': 'HKFC', 'team': 'G', 'display_name': 'HKFC G', 'league': '4th Division'},
|
|
{'club': 'Dutch', 'team': 'B', 'display_name': 'Dutch B', 'league': '4th Division'},
|
|
{'club': 'RHOBA', 'team': 'C', 'display_name': 'RHOBA C', 'league': '4th Division'},
|
|
{'club': 'HKUHC', 'team': 'A', 'display_name': 'HKUHC A', 'league': '4th Division'},
|
|
{'club': 'Kaitiaki', 'team': 'A', 'display_name': 'Kaitiaki', 'league': '4th Division'},
|
|
|
|
# 5th Division
|
|
{'club': 'KCC', 'team': 'D', 'display_name': 'KCC D', 'league': '5th Division'},
|
|
{'club': 'Kai Tak', 'team': 'C', 'display_name': 'Kai Tak C', 'league': '5th Division'},
|
|
{'club': 'Dragons', 'team': 'B', 'display_name': 'Dragons B', 'league': '5th Division'},
|
|
{'club': 'Antlers', 'team': 'C', 'display_name': 'Antlers C', 'league': '5th Division'},
|
|
{'club': 'Valley', 'team': 'E', 'display_name': 'Valley E', 'league': '5th Division'},
|
|
{'club': 'Elite', 'team': 'C', 'display_name': 'Elite C', 'league': '5th Division'},
|
|
{'club': 'HKFC', 'team': 'H', 'display_name': 'HKFC H', 'league': '5th Division'},
|
|
{'club': 'Aquila', 'team': 'B', 'display_name': 'Aquila B', 'league': '5th Division'},
|
|
{'club': 'Sirius', 'team': 'B', 'display_name': 'Sirius B', 'league': '5th Division'},
|
|
{'club': 'Marcellin', 'team': 'A', 'display_name': 'Marcellin A', 'league': '5th Division'},
|
|
{'club': 'Recreio', 'team': 'B', 'display_name': 'Recreio B', 'league': '5th Division'},
|
|
{'club': 'Diocesan', 'team': 'B', 'display_name': 'Diocesan B', 'league': '5th Division'},
|
|
|
|
# 6th Division
|
|
{'club': 'Rhino', 'team': 'B', 'display_name': 'Rhino B', 'league': '6th Division'},
|
|
{'club': 'Skyers', 'team': 'A', 'display_name': 'Skyers A', 'league': '6th Division'},
|
|
{'club': 'JR', 'team': 'A', 'display_name': 'JR', 'league': '6th Division'},
|
|
{'club': 'HKCC', 'team': 'D', 'display_name': 'HKCC D', 'league': '6th Division'},
|
|
{'club': 'KCC', 'team': 'E', 'display_name': 'KCC E', 'league': '6th Division'},
|
|
{'club': 'HKJ', 'team': 'C', 'display_name': 'HKJ C', 'league': '6th Division'},
|
|
{'club': 'IUHK', 'team': 'A', 'display_name': 'IUHK A', 'league': '6th Division'},
|
|
{'club': 'Valley', 'team': 'F', 'display_name': 'Valley F', 'league': '6th Division'},
|
|
{'club': '144U', 'team': 'A', 'display_name': '144U A', 'league': '6th Division'},
|
|
{'club': 'HKU', 'team': 'A', 'display_name': 'HKU A', 'league': '6th Division'},
|
|
]
|
|
|
|
for team_data in teams_data:
|
|
# Check if team already exists
|
|
sql_check = text("SELECT club, team FROM teams WHERE club = :club AND team = :team")
|
|
existing = sql_read(sql_check, {'club': team_data['club'], 'team': team_data['team']})
|
|
|
|
if not existing:
|
|
sql = text("INSERT INTO teams (club, team, display_name, league) VALUES (:club, :team, :display_name, :league)")
|
|
sql_write(sql, team_data)
|
|
imported_teams += 1
|
|
|
|
if form.import_players.data:
|
|
# Import sample players for HKFC C team
|
|
players_data = [
|
|
{'player_number': 1, 'player_forenames': 'John', 'player_surname': 'Smith', 'player_nickname': 'Smithers', 'player_team': 'HKFC C'},
|
|
{'player_number': 2, 'player_forenames': 'Mike', 'player_surname': 'Jones', 'player_nickname': 'Jonesy', 'player_team': 'HKFC C'},
|
|
{'player_number': 3, 'player_forenames': 'David', 'player_surname': 'Brown', 'player_nickname': 'Brownie', 'player_team': 'HKFC C'},
|
|
{'player_number': 4, 'player_forenames': 'Chris', 'player_surname': 'Wilson', 'player_nickname': 'Willy', 'player_team': 'HKFC C'},
|
|
{'player_number': 5, 'player_forenames': 'Tom', 'player_surname': 'Taylor', 'player_nickname': 'Tayls', 'player_team': 'HKFC C'},
|
|
]
|
|
|
|
for player_data in players_data:
|
|
# Check if player already exists
|
|
sql_check = text("SELECT playernumber FROM _hkfc_players WHERE playernumber = :player_number")
|
|
existing = sql_read(sql_check, {'player_number': player_data['player_number']})
|
|
|
|
if not existing:
|
|
sql = text("INSERT INTO _hkfc_players (playernumber, playerforenames, playersurname, playernickname, playerteam) VALUES (:player_number, :forenames, :surname, :nickname, :team)")
|
|
sql_write(sql, player_data)
|
|
imported_players += 1
|
|
|
|
flash(f'Import completed! {imported_clubs} clubs, {imported_teams} teams, {imported_players} players imported.', 'success')
|
|
return redirect(url_for('data_import'))
|
|
elif form.cancel.data:
|
|
return redirect(url_for('data_import'))
|
|
|
|
return render_template('data_import.html', form=form)
|
|
|
|
|
|
@app.route('/admin/import/clubs/select', methods=['GET', 'POST'])
|
|
@basic_auth.required
|
|
def club_selection():
|
|
"""Club selection page for importing specific clubs"""
|
|
form = ClubSelectionForm()
|
|
|
|
# Fetch clubs from the website
|
|
try:
|
|
clubs = get_hk_hockey_clubs()
|
|
if not clubs:
|
|
# Fallback to predefined clubs if scraping fails
|
|
clubs = [
|
|
{'name': 'Hong Kong Football Club', 'abbreviation': 'HKFC', 'teams': ['A', 'B', 'C', 'F', 'G', 'H'], 'convenor': None, 'email': None},
|
|
{'name': 'Kowloon Cricket Club', 'abbreviation': 'KCC', 'teams': ['A', 'B', 'C', 'D', 'E'], 'convenor': None, 'email': None},
|
|
{'name': 'United Services Recreation Club', 'abbreviation': 'USRC', 'teams': ['A', 'B', 'C'], 'convenor': None, 'email': None},
|
|
{'name': 'Valley Fort Sports Club', 'abbreviation': 'Valley', 'teams': ['A', 'B', 'C', 'D', 'E', 'F'], 'convenor': None, 'email': None},
|
|
{'name': 'South China Sports Club', 'abbreviation': 'SSSC', 'teams': ['C'], 'convenor': None, 'email': None},
|
|
{'name': 'Dragons Hockey Club', 'abbreviation': 'Dragons', 'teams': ['A', 'B'], 'convenor': None, 'email': None},
|
|
{'name': 'Kai Tak Sports Club', 'abbreviation': 'Kai Tak', 'teams': ['B', 'C'], 'convenor': None, 'email': None},
|
|
{'name': 'Royal Hong Kong Regiment Officers and Businessmen Association', 'abbreviation': 'RHOBA', 'teams': ['A', 'B', 'C'], 'convenor': None, 'email': None},
|
|
{'name': 'Elite Hockey Club', 'abbreviation': 'Elite', 'teams': ['B', 'C'], 'convenor': None, 'email': None},
|
|
{'name': 'Aquila Hockey Club', 'abbreviation': 'Aquila', 'teams': ['A', 'B'], 'convenor': None, 'email': None},
|
|
{'name': 'Hong Kong Jockey Club', 'abbreviation': 'HKJ', 'teams': ['B', 'C'], 'convenor': None, 'email': None},
|
|
{'name': 'Sirius Hockey Club', 'abbreviation': 'Sirius', 'teams': ['A', 'B'], 'convenor': None, 'email': None},
|
|
{'name': 'Shaheen Hockey Club', 'abbreviation': 'Shaheen', 'teams': ['B'], 'convenor': None, 'email': None},
|
|
{'name': 'Diocesan Boys School', 'abbreviation': 'Diocesan', 'teams': ['B'], 'convenor': None, 'email': None},
|
|
{'name': 'Rhino Hockey Club', 'abbreviation': 'Rhino', 'teams': ['B'], 'convenor': None, 'email': None},
|
|
{'name': 'Khalsa Hockey Club', 'abbreviation': 'Khalsa', 'teams': ['C'], 'convenor': None, 'email': None},
|
|
{'name': 'Hong Kong Cricket Club', 'abbreviation': 'HKCC', 'teams': ['C', 'D'], 'convenor': None, 'email': None},
|
|
{'name': 'Hong Kong Police Force', 'abbreviation': 'Police', 'teams': ['A'], 'convenor': None, 'email': None},
|
|
{'name': 'Recreio Hockey Club', 'abbreviation': 'Recreio', 'teams': ['A', 'B'], 'convenor': None, 'email': None},
|
|
{'name': 'Correctional Services Department', 'abbreviation': 'CSD', 'teams': ['A'], 'convenor': None, 'email': None},
|
|
{'name': 'Dutch Hockey Club', 'abbreviation': 'Dutch', 'teams': ['B'], 'convenor': None, 'email': None},
|
|
{'name': 'Hong Kong University Hockey Club', 'abbreviation': 'HKUHC', 'teams': ['A'], 'convenor': None, 'email': None},
|
|
{'name': 'Kaitiaki Hockey Club', 'abbreviation': 'Kaitiaki', 'teams': ['A'], 'convenor': None, 'email': None},
|
|
{'name': 'Antlers Hockey Club', 'abbreviation': 'Antlers', 'teams': ['C'], 'convenor': None, 'email': None},
|
|
{'name': 'Marcellin Hockey Club', 'abbreviation': 'Marcellin', 'teams': ['A'], 'convenor': None, 'email': None},
|
|
{'name': 'Skyers Hockey Club', 'abbreviation': 'Skyers', 'teams': ['A'], 'convenor': None, 'email': None},
|
|
{'name': 'JR Hockey Club', 'abbreviation': 'JR', 'teams': ['A'], 'convenor': None, 'email': None},
|
|
{'name': 'International University of Hong Kong', 'abbreviation': 'IUHK', 'teams': ['A'], 'convenor': None, 'email': None},
|
|
{'name': '144 United Hockey Club', 'abbreviation': '144U', 'teams': ['A'], 'convenor': None, 'email': None},
|
|
{'name': 'Hong Kong University', 'abbreviation': 'HKU', 'teams': ['A'], 'convenor': None, 'email': None},
|
|
]
|
|
except Exception as e:
|
|
flash(f'Error fetching clubs: {str(e)}', 'error')
|
|
clubs = []
|
|
|
|
if request.method == 'POST':
|
|
if form.select_all.data:
|
|
# Select all clubs
|
|
selected_clubs = [club['name'] for club in clubs]
|
|
return render_template('club_selection.html', form=form, clubs=clubs, selected_clubs=selected_clubs)
|
|
|
|
elif form.select_none.data:
|
|
# Select no clubs
|
|
return render_template('club_selection.html', form=form, clubs=clubs, selected_clubs=[])
|
|
|
|
elif form.import_selected.data:
|
|
# Import selected clubs
|
|
selected_clubs = request.form.getlist('selected_clubs')
|
|
|
|
if not selected_clubs:
|
|
flash('No clubs selected for import!', 'error')
|
|
return render_template('club_selection.html', form=form, clubs=clubs, selected_clubs=[])
|
|
|
|
imported_count = 0
|
|
skipped_count = 0
|
|
|
|
for club_name in selected_clubs:
|
|
# Find the club data
|
|
club_data = next((club for club in clubs if club['name'] == club_name), None)
|
|
if not club_data:
|
|
continue
|
|
|
|
# Check if club already exists
|
|
sql_check = text("SELECT hockey_club FROM clubs WHERE hockey_club = :club_name")
|
|
existing = sql_read(sql_check, {'club_name': club_name})
|
|
|
|
if existing:
|
|
skipped_count += 1
|
|
continue
|
|
|
|
# Import the club
|
|
logo_url = f"/static/images/clubs/{club_name.lower().replace(' ', '_').replace('.', '').replace(',', '')}_logo.png"
|
|
sql = text("INSERT INTO clubs (hockey_club, logo_url) VALUES (:club_name, :logo_url)")
|
|
sql_write(sql, {'club_name': club_name, 'logo_url': logo_url})
|
|
imported_count += 1
|
|
|
|
if imported_count > 0:
|
|
flash(f'Successfully imported {imported_count} clubs!', 'success')
|
|
if skipped_count > 0:
|
|
flash(f'Skipped {skipped_count} clubs that already exist.', 'info')
|
|
|
|
return redirect(url_for('club_management'))
|
|
|
|
elif form.cancel.data:
|
|
return redirect(url_for('data_import'))
|
|
|
|
return render_template('club_selection.html', form=form, clubs=clubs, selected_clubs=[])
|
|
|
|
|
|
@app.route('/admin/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/squad/submit', methods=['POST'])
|
|
@basic_auth.required
|
|
def match_squad_submit():
|
|
"""Process squad selection"""
|
|
_playerNumbers = request.form.getlist('playerNumber')
|
|
|
|
if not _playerNumbers:
|
|
flash('No players selected!', 'error')
|
|
return redirect(url_for('match_squad'))
|
|
|
|
for _playerNumber in _playerNumbers:
|
|
sql = text("INSERT INTO _hkfcc_matchsquad (playernumber, playerforenames, playersurname, playernickname) SELECT playernumber, playerforenames, playersurname, playernickname FROM _hkfc_players WHERE playernumber = :player_number")
|
|
sql_write(sql, {'player_number': _playerNumber})
|
|
|
|
flash(f'Successfully added {len(_playerNumbers)} player(s) to the squad!', 'success')
|
|
|
|
sql2 = text("SELECT playernumber, playerforenames, playersurname, playernickname FROM _hkfcc_matchsquad")
|
|
players = sql_read(sql2)
|
|
table = matchSquadTable(players)
|
|
table.border = True
|
|
table.classes = ['table-striped', 'table-condensed', 'table-hover']
|
|
return render_template('match_squad_selected.html', table=table)
|
|
|
|
|
|
@app.route('/admin/squad/list')
|
|
@basic_auth.required
|
|
def match_squad_list():
|
|
"""Display current squad list"""
|
|
sql = text("SELECT playernumber, playerforenames, playersurname, playernickname FROM _hkfcc_matchsquad")
|
|
players = sql_read(sql)
|
|
table = matchSquadTable(players)
|
|
table.border = True
|
|
table.classes = ['table-striped', 'table-condensed', 'table-hover']
|
|
return render_template('match_squad_selected.html', table=table)
|
|
|
|
|
|
@app.route('/admin/squad/remove', methods=['POST'])
|
|
@basic_auth.required
|
|
def delPlayerFromSquad():
|
|
"""Remove player from squad"""
|
|
_playerNumber = request.args.get('playerNumber')
|
|
if not _playerNumber:
|
|
flash('Player number not provided', 'error')
|
|
return redirect(url_for('match_squad_list'))
|
|
|
|
sql = text("DELETE FROM _hkfcc_matchsquad WHERE playernumber = :player_number")
|
|
sql_write(sql, {'player_number': _playerNumber})
|
|
flash(f'Player #{_playerNumber} removed from squad', 'success')
|
|
return redirect(url_for('match_squad_list'))
|
|
|
|
|
|
@app.route('/admin/squad/reset')
|
|
@basic_auth.required
|
|
def matchSquadReset():
|
|
"""Reset squad for new match"""
|
|
_matchNumber = str(mySettings('fixture'))
|
|
print(_matchNumber)
|
|
|
|
try:
|
|
# First, check if there are any players in the current squad
|
|
check_sql = text("SELECT COUNT(*) as count FROM _hkfcC_matchSquad")
|
|
result = sql_read(check_sql)
|
|
squad_count = result[0]['count'] if result else 0
|
|
|
|
if squad_count > 0:
|
|
# Rename current squad table
|
|
sql1 = text(f"RENAME TABLE _hkfcC_matchSquad TO _hkfcC_matchSquad_{_matchNumber}")
|
|
sql_write(sql1)
|
|
|
|
# Create new empty squad table
|
|
sql2 = text("CREATE TABLE _hkfcC_matchSquad (playerNumber smallint UNIQUE, playerForenames varchar(50), playerSurname varchar(30), playerNickname varchar(30) NOT NULL, PRIMARY KEY (playerNumber))")
|
|
sql_write(sql2)
|
|
|
|
# Update fixture number
|
|
sql3 = text("UPDATE motmAdminSettings SET prevFixture = :match_number")
|
|
sql_write_static(sql3, {'match_number': _matchNumber})
|
|
|
|
flash(f'Squad reset successfully! {squad_count} players archived for match {_matchNumber}', 'success')
|
|
else:
|
|
flash('No players in current squad to reset', 'info')
|
|
|
|
except Exception as e:
|
|
flash(f'Error resetting squad: {str(e)}', 'error')
|
|
|
|
return render_template('match_squad_reset.html')
|
|
|
|
|
|
@app.route('/admin/stats', methods=['GET', 'POST'])
|
|
@basic_auth.required
|
|
def stats_admin():
|
|
"""Admin page for managing goals and assists statistics"""
|
|
form = goalsAssistsForm()
|
|
sql = "SELECT date, homeTeam, awayTeam, venue, fixtureNumber FROM hockeyFixtures WHERE homeTeam='HKFC C' OR awayTeam='HKFC C'"
|
|
matches = sql_read(sql)
|
|
form.match.choices = [(match['fixtureNumber'], match['date']) for match in matches]
|
|
sql2 = "SELECT playerNumber, playerNickname FROM _hkfcC_matchSquad"
|
|
players = sql_read(sql2)
|
|
return render_template('goals_assists_admin.html', data=players, form=form)
|
|
|
|
|
|
@app.route('/admin/stats/submit', methods=['POST'])
|
|
@basic_auth.required
|
|
def goalsAssistsSubmit():
|
|
"""Process goals and assists statistics"""
|
|
try:
|
|
data = request.form
|
|
playerName = request.form.getlist('playerName')
|
|
playerNumber = request.form.getlist('playerNumber')
|
|
assists = request.form.getlist('assists')
|
|
goals = request.form.getlist('goals')
|
|
match = request.form['match']
|
|
for idx, player in enumerate(playerNumber):
|
|
sql = "INSERT INTO _hkfc_c_motm (playerNumber, playerName, assistsTotal, goalsTotal, assists_" + match + ", goals_" + match + ") SELECT playerNumber, playerNickname, '" + assists[idx] + "', '" + goals[idx] + "', '" + assists[idx] + "', '" + goals[idx] + "' FROM _HKFC_players WHERE playerNumber='" + player + "' ON DUPLICATE KEY UPDATE assistsTotal = assistsTotal + " + assists[idx] + ", goalsTotal = goalsTotal + " + goals[idx] + ", assists_" + match + " = " + assists[idx] + ", goals_" + match + " = " + goals[idx] + ""
|
|
sql_write(sql)
|
|
except Exception as e:
|
|
print(e)
|
|
finally:
|
|
return render_template('goals_thanks.html', data=data)
|
|
|
|
|
|
# ==================== API ENDPOINTS ====================
|
|
|
|
@app.route('/admin/api/next-fixture')
|
|
@basic_auth.required
|
|
def get_next_fixture():
|
|
"""API endpoint to fetch the next HKFC C fixture from Hockey Hong Kong website"""
|
|
try:
|
|
fixture = get_next_hkfc_c_fixture()
|
|
|
|
if fixture:
|
|
# Get opponent club information
|
|
opponent_club_info = get_opponent_club_info(fixture['opponent'])
|
|
|
|
# Format the fixture data for JSON response
|
|
fixture_data = {
|
|
'success': True,
|
|
'date': fixture['date'].strftime('%Y-%m-%d'),
|
|
'date_formatted': fixture['date'].strftime('%A, %d %B %Y'),
|
|
'time': fixture['time'],
|
|
'venue': fixture['venue'],
|
|
'opponent': fixture['opponent'],
|
|
'opponent_club': get_opponent_club_name(fixture['opponent']),
|
|
'opponent_club_info': opponent_club_info,
|
|
'is_home': fixture['is_home'],
|
|
'home_team': fixture['home_team'],
|
|
'away_team': fixture['away_team'],
|
|
'division': fixture['division']
|
|
}
|
|
return jsonify(fixture_data)
|
|
else:
|
|
return jsonify({
|
|
'success': False,
|
|
'message': 'No upcoming fixtures found for HKFC C'
|
|
})
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'message': f'Error fetching fixture: {str(e)}'
|
|
})
|
|
|
|
|
|
@app.route('/admin/api/clubs')
|
|
@basic_auth.required
|
|
def get_hockey_clubs():
|
|
"""API endpoint to fetch clubs from Hockey Hong Kong website"""
|
|
try:
|
|
clubs = get_hk_hockey_clubs()
|
|
|
|
if clubs:
|
|
# Format the club data for JSON response
|
|
club_data = {
|
|
'success': True,
|
|
'clubs': clubs,
|
|
'count': len(clubs)
|
|
}
|
|
return jsonify(club_data)
|
|
else:
|
|
return jsonify({
|
|
'success': False,
|
|
'message': 'No clubs found on Hockey Hong Kong website'
|
|
})
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'message': f'Error fetching clubs: {str(e)}'
|
|
})
|
|
|
|
|
|
@app.route('/admin/api/import-clubs', methods=['POST'])
|
|
@basic_auth.required
|
|
def import_clubs():
|
|
"""API endpoint to import clubs from Hockey Hong Kong website"""
|
|
try:
|
|
clubs = get_hk_hockey_clubs()
|
|
imported_count = 0
|
|
updated_count = 0
|
|
|
|
for club in clubs:
|
|
club_name = club['name']
|
|
abbreviation = club.get('abbreviation', '')
|
|
|
|
# Check if club already exists
|
|
sql_check = text("SELECT id FROM clubs WHERE hockey_club = :club_name")
|
|
existing = sql_read(sql_check, {'club_name': club_name})
|
|
|
|
if existing:
|
|
# Update existing club
|
|
sql_update = text("UPDATE clubs SET logo_url = :logo_url WHERE hockey_club = :club_name")
|
|
logo_url = f"/static/images/clubs/{club_name.lower().replace(' ', '_').replace('.', '').replace(',', '')}_logo.png"
|
|
sql_write(sql_update, {'logo_url': logo_url, 'club_name': club_name})
|
|
updated_count += 1
|
|
else:
|
|
# Insert new club
|
|
sql_insert = text("INSERT INTO clubs (hockey_club, logo_url) VALUES (:club_name, :logo_url)")
|
|
logo_url = f"/static/images/clubs/{club_name.lower().replace(' ', '_').replace('.', '').replace(',', '')}_logo.png"
|
|
sql_write(sql_insert, {'club_name': club_name, 'logo_url': logo_url})
|
|
imported_count += 1
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': f'Successfully imported {imported_count} new clubs and updated {updated_count} existing clubs',
|
|
'imported': imported_count,
|
|
'updated': updated_count,
|
|
'total': len(clubs)
|
|
})
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'message': f'Error importing clubs: {str(e)}'
|
|
})
|
|
|
|
|
|
@app.route('/admin/api/match-opponent/<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 WHERE userid = 'admin'")
|
|
date_result = sql_read_static(sql_date)
|
|
|
|
if not date_result:
|
|
return json.dumps([])
|
|
|
|
_matchDate = str(date_result[0]['nextdate']).replace('-', '')
|
|
print(f"Match date: {_matchDate}")
|
|
|
|
# Query for votes with the current match date
|
|
sql = text(f"SELECT playername, motm_{_matchDate}, dotd_{_matchDate} FROM _hkfc_c_motm WHERE (motm_{_matchDate} > 0) OR (dotd_{_matchDate} > 0)")
|
|
print(f"SQL: {sql}")
|
|
rows = sql_read(sql)
|
|
print(f"Results: {rows}")
|
|
|
|
# Format the results for the chart
|
|
formatted_results = []
|
|
for row in rows:
|
|
formatted_results.append({
|
|
'playerName': row['playername'],
|
|
f'motm_{_matchDate}': row[f'motm_{_matchDate}'],
|
|
f'dotd_{_matchDate}': row[f'dotd_{_matchDate}']
|
|
})
|
|
|
|
return jsonify(formatted_results)
|
|
|
|
|
|
@app.route('/api/poty-results')
|
|
def poty_results():
|
|
"""API endpoint for Player of the Year results with dynamic totals"""
|
|
# Get all players with their fixture-specific columns
|
|
sql = text("SELECT * FROM _hkfc_c_motm")
|
|
rows = sql_read(sql)
|
|
|
|
# Calculate dynamic totals for each player
|
|
results = []
|
|
for player in rows:
|
|
motm_total = 0
|
|
dotd_total = 0
|
|
|
|
# Sum all fixture-specific columns
|
|
for col_name in player.keys():
|
|
if col_name.startswith('motm_') and col_name != 'motmtotal':
|
|
motm_total += player[col_name] or 0
|
|
elif col_name.startswith('dotd_') and col_name != 'dotdtotal':
|
|
dotd_total += player[col_name] or 0
|
|
|
|
# Only include players with votes
|
|
if motm_total > 0 or dotd_total > 0:
|
|
results.append({
|
|
'playername': player['playername'],
|
|
'motmtotal': motm_total,
|
|
'dotdtotal': dotd_total
|
|
})
|
|
|
|
print(f"Dynamic POTY Results: {results}")
|
|
return jsonify(results)
|
|
|
|
|
|
@app.route('/admin/voting')
|
|
@basic_auth.required
|
|
def voting_chart():
|
|
"""Admin page for viewing voting charts"""
|
|
# Get the current match date from admin settings
|
|
sql_date = text("SELECT nextdate FROM motmadminsettings WHERE userid = 'admin'")
|
|
date_result = sql_read_static(sql_date)
|
|
|
|
if date_result:
|
|
matchDate = str(date_result[0]['nextdate']).replace('-', '')
|
|
else:
|
|
matchDate = '20251012' # Default fallback
|
|
|
|
return render_template('vote_chart.html', _matchDate=matchDate)
|
|
|
|
|
|
@app.route('/admin/poty')
|
|
@basic_auth.required
|
|
def poty_chart():
|
|
"""Admin page for Player of the Year chart"""
|
|
return render_template('poty_chart.html')
|
|
|
|
|
|
# ==================== DATABASE SETUP SECTION ====================
|
|
|
|
@app.route('/admin/database-setup', methods=['GET', 'POST'])
|
|
@basic_auth.required
|
|
def database_setup():
|
|
"""Admin page for database setup and configuration"""
|
|
form = DatabaseSetupForm()
|
|
|
|
if request.method == 'POST':
|
|
if form.test_connection.data:
|
|
# Test database connection
|
|
form_data = {
|
|
'database_type': form.database_type.data,
|
|
'sqlite_database_path': form.sqlite_database_path.data,
|
|
'mysql_host': form.mysql_host.data,
|
|
'mysql_port': form.mysql_port.data,
|
|
'mysql_database': form.mysql_database.data,
|
|
'mysql_username': form.mysql_username.data,
|
|
'mysql_password': form.mysql_password.data,
|
|
'mysql_charset': form.mysql_charset.data,
|
|
'postgres_host': form.postgres_host.data,
|
|
'postgres_port': form.postgres_port.data,
|
|
'postgres_database': form.postgres_database.data,
|
|
'postgres_username': form.postgres_username.data,
|
|
'postgres_password': form.postgres_password.data,
|
|
}
|
|
|
|
success, message = db_config_manager.test_connection(form_data)
|
|
if success:
|
|
flash(f'✅ {message}', 'success')
|
|
else:
|
|
flash(f'❌ {message}', 'error')
|
|
|
|
elif form.save_config.data:
|
|
# Save configuration
|
|
form_data = {
|
|
'database_type': form.database_type.data,
|
|
'sqlite_database_path': form.sqlite_database_path.data,
|
|
'mysql_host': form.mysql_host.data,
|
|
'mysql_port': form.mysql_port.data,
|
|
'mysql_database': form.mysql_database.data,
|
|
'mysql_username': form.mysql_username.data,
|
|
'mysql_password': form.mysql_password.data,
|
|
'mysql_charset': form.mysql_charset.data,
|
|
'postgres_host': form.postgres_host.data,
|
|
'postgres_port': form.postgres_port.data,
|
|
'postgres_database': form.postgres_database.data,
|
|
'postgres_username': form.postgres_username.data,
|
|
'postgres_password': form.postgres_password.data,
|
|
}
|
|
|
|
try:
|
|
db_config_manager.save_config(form_data)
|
|
flash('✅ Database configuration saved successfully!', 'success')
|
|
except Exception as e:
|
|
flash(f'❌ Failed to save configuration: {str(e)}', 'error')
|
|
|
|
elif form.initialize_database.data:
|
|
# Initialize database
|
|
try:
|
|
success, message = db_config_manager.initialize_database(
|
|
create_sample_data=form.create_sample_data.data
|
|
)
|
|
if success:
|
|
flash(f'✅ {message}', 'success')
|
|
else:
|
|
flash(f'❌ {message}', 'error')
|
|
except Exception as e:
|
|
flash(f'❌ Database initialization failed: {str(e)}', 'error')
|
|
|
|
# Load current configuration for display
|
|
current_config = db_config_manager.get_config_dict()
|
|
|
|
# Populate form with current configuration (only for GET requests or after POST processing)
|
|
for field_name, value in current_config.items():
|
|
if hasattr(form, field_name):
|
|
getattr(form, field_name).data = value
|
|
|
|
return render_template('database_setup.html', form=form, current_config=current_config)
|
|
|
|
|
|
@app.route('/admin/database-status')
|
|
@basic_auth.required
|
|
def database_status():
|
|
"""Admin page showing current database status and configuration"""
|
|
try:
|
|
# Reload database configuration to get latest settings
|
|
from database import DatabaseConfig
|
|
current_db_config = DatabaseConfig()
|
|
engine = current_db_config.engine
|
|
|
|
# Test connection
|
|
with engine.connect() as conn:
|
|
result = conn.execute(text("SELECT 1"))
|
|
result.fetchone()
|
|
|
|
connection_status = "✅ Connected"
|
|
|
|
# Get database info
|
|
db_info = {
|
|
'database_url': str(current_db_config.database_url),
|
|
'database_type': os.getenv('DATABASE_TYPE', 'unknown'),
|
|
'connection_status': connection_status
|
|
}
|
|
|
|
# Try to get table count
|
|
try:
|
|
from database import Base
|
|
table_count = len(Base.metadata.tables)
|
|
db_info['table_count'] = table_count
|
|
except:
|
|
db_info['table_count'] = 'Unknown'
|
|
|
|
except Exception as e:
|
|
db_info = {
|
|
'database_url': 'Not configured',
|
|
'database_type': 'Unknown',
|
|
'connection_status': f'❌ Connection failed: {str(e)}',
|
|
'table_count': 'Unknown'
|
|
}
|
|
|
|
return render_template('database_status.html', db_info=db_info)
|
|
|
|
|
|
# ==================== 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)
|