Add extra DB functions

This commit is contained in:
Jonny Ervine 2025-10-04 16:47:40 +08:00
parent 131f17947c
commit 0c60a4b4d8
3 changed files with 489 additions and 19 deletions

View File

@ -111,7 +111,7 @@ def motm_vote(randomUrlSuffix):
# Get match comments
sql5 = text("SELECT comment FROM _motmcomments WHERE matchDate = :match_date ORDER BY RANDOM() LIMIT 1")
comment_result = sql_read(sql5, {'match_date': nextInfo[0]['nextdate']})
comment_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()
@ -120,7 +120,9 @@ def motm_vote(randomUrlSuffix):
if nextInfo and nextInfo[0].get('motmurlsuffix'):
randomSuff = nextInfo[0]['motmurlsuffix']
if randomSuff == randomUrlSuffix:
return render_template('motm_vote.html', data=rows, comment=comment, formatDate=formatDate, matchNumber=nextInfo[0].get('nextfixture', ''), oppo=oppo, hkfcLogo=hkfcLogo, oppoLogo=oppoLogo, dotdURL=dotdURL, motmURL=motmURL, form=form)
# 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:
@ -144,8 +146,8 @@ def match_comments():
_comment = request.form['matchComment']
if _comment != 'Optional comments added here':
_fixed_comment = _comment.replace("'", "\\'")
sql3 = text("INSERT INTO _motmcomments (matchDate, opposition, comment) VALUES (:comment_date, :opposition, :comment)")
sql_write(sql3, {'comment_date': commentDate, 'opposition': _oppo, 'comment': _fixed_comment})
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)
@ -188,30 +190,28 @@ def vote_thanks():
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
# Update MOTM vote - use PostgreSQL UPSERT syntax (don't update totals)
sql_motm = text(f"""
INSERT INTO _hkfc_c_motm (playernumber, playername, motmtotal, {motm_col})
VALUES (:player_num, :player_name, 1, 1)
INSERT INTO _hkfc_c_motm (playernumber, playername, {motm_col})
VALUES (:player_num, :player_name, 1)
ON CONFLICT (playernumber) DO UPDATE SET
motmTotal = _hkfc_c_motm.motmTotal + 1,
{motm_col} = _hkfc_c_motm.{motm_col} + 1
""")
sql_write(sql_motm, {'player_num': _motm, 'player_name': motm_name})
# Update DotD vote - use PostgreSQL UPSERT syntax
# Update DotD vote - use PostgreSQL UPSERT syntax (don't update totals)
sql_dotd = text(f"""
INSERT INTO _hkfc_c_motm (playernumber, playername, dotdtotal, {dotd_col})
VALUES (:player_num, :player_name, 1, 1)
INSERT INTO _hkfc_c_motm (playernumber, playername, {dotd_col})
VALUES (:player_num, :player_name, 1)
ON CONFLICT (playernumber) DO UPDATE SET
dotdTotal = _hkfc_c_motm.dotdTotal + 1,
{dotd_col} = _hkfc_c_motm.{dotd_col} + 1
""")
sql_write(sql_dotd, {'player_num': _dotd, 'player_name': dotd_name})
# Handle comments
if _comments and _comments != "Optional comments added here":
sql3 = text("INSERT INTO _motmcomments (matchDate, opposition, comment) VALUES (:match_date, :opposition, :comment)")
sql_write(sql3, {'match_date': _matchDate, 'opposition': _oppo, 'comment': _fixed_comments})
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:
@ -919,6 +919,174 @@ def club_selection():
return render_template('club_selection.html', form=form, clubs=clubs, selected_clubs=[])
@app.route('/admin/motm/manage', methods=['GET', 'POST'])
@basic_auth.required
def motm_management():
"""Manage MOTM/DotD counts and reset functionality"""
if request.method == 'POST':
action = request.form.get('action')
if action == 'reset_fixture':
fixture_date = request.form.get('fixture_date')
if fixture_date:
motm_col = f'motm_{fixture_date}'
dotd_col = f'dotd_{fixture_date}'
# Reset fixture-specific columns
sql_reset = text(f"""
UPDATE _hkfc_c_motm
SET {motm_col} = 0, {dotd_col} = 0
WHERE {motm_col} > 0 OR {dotd_col} > 0
""")
sql_write(sql_reset)
flash(f'Reset MOTM/DotD counts for fixture {fixture_date}', 'success')
elif action == 'reset_totals':
# Reset all total columns
sql_reset_totals = text("""
UPDATE _hkfc_c_motm
SET motmtotal = 0, dotdtotal = 0, assiststotal = 0, goalstotal = 0
""")
sql_write(sql_reset_totals)
flash('Reset all MOTM/DotD totals', 'success')
elif action == 'reset_all':
# Reset everything - dynamically reset all fixture columns
sql_columns = text("""
SELECT column_name
FROM information_schema.columns
WHERE table_name = '_hkfc_c_motm'
AND (column_name LIKE 'motm_%' OR column_name LIKE 'dotd_%')
AND column_name NOT LIKE '%total'
""")
columns = sql_read(sql_columns)
# Build dynamic SET clause
set_clauses = ['motmtotal = 0', 'dotdtotal = 0', 'assiststotal = 0', 'goalstotal = 0']
for col in columns:
set_clauses.append(f"{col['column_name']} = 0")
sql_reset_all = text(f"""
UPDATE _hkfc_c_motm
SET {', '.join(set_clauses)}
""")
sql_write(sql_reset_all)
flash('Reset all MOTM/DotD data', 'success')
elif action == 'reset_player_fixture':
player_number = request.form.get('player_number')
fixture_date = request.form.get('fixture_date')
if player_number and fixture_date:
motm_col = f'motm_{fixture_date}'
dotd_col = f'dotd_{fixture_date}'
# Reset specific player's fixture counts
sql_reset_player = text(f"""
UPDATE _hkfc_c_motm
SET {motm_col} = 0, {dotd_col} = 0
WHERE playernumber = :player_number
""")
sql_write(sql_reset_player, {'player_number': player_number})
flash(f'Reset MOTM/DotD counts for player #{player_number} in fixture {fixture_date}', 'success')
elif action == 'drop_column':
column_name = request.form.get('column_name')
if column_name:
try:
sql_drop = text(f"ALTER TABLE _hkfc_c_motm DROP COLUMN {column_name}")
sql_write(sql_drop)
flash(f'Successfully dropped column {column_name}', 'success')
except Exception as e:
flash(f'Error dropping column {column_name}: {str(e)}', 'error')
elif action == 'drop_fixture_columns':
fixture_date = request.form.get('fixture_date')
if fixture_date:
motm_col = f'motm_{fixture_date}'
dotd_col = f'dotd_{fixture_date}'
try:
sql_drop_motm = text(f"ALTER TABLE _hkfc_c_motm DROP COLUMN {motm_col}")
sql_drop_dotd = text(f"ALTER TABLE _hkfc_c_motm DROP COLUMN {dotd_col}")
sql_write(sql_drop_motm)
sql_write(sql_drop_dotd)
flash(f'Successfully dropped columns for fixture {fixture_date}', 'success')
except Exception as e:
flash(f'Error dropping fixture columns: {str(e)}', 'error')
elif action == 'sync_totals':
# Sync stored totals with calculated totals
sql_data = text("SELECT * FROM _hkfc_c_motm")
players = sql_read(sql_data)
updated_count = 0
for player in players:
motm_total = 0
dotd_total = 0
# Calculate totals from fixture columns
for col_name in player.keys():
if col_name.startswith('motm_') and col_name != 'motmtotal':
motm_total += player[col_name] or 0
elif col_name.startswith('dotd_') and col_name != 'dotdtotal':
dotd_total += player[col_name] or 0
# Update stored totals if they differ
if player.get('motmtotal', 0) != motm_total or player.get('dotdtotal', 0) != dotd_total:
sql_update = text("UPDATE _hkfc_c_motm SET motmtotal = :motm_total, dotdtotal = :dotd_total WHERE playernumber = :player_num")
sql_write(sql_update, {
'motm_total': motm_total,
'dotd_total': dotd_total,
'player_num': player['playernumber']
})
updated_count += 1
flash(f'Synced totals for {updated_count} players', 'success')
# Get all fixture columns
sql_columns = text("""
SELECT column_name
FROM information_schema.columns
WHERE table_name = '_hkfc_c_motm'
AND column_name LIKE 'motm_%' OR column_name LIKE 'dotd_%'
ORDER BY column_name
""")
columns = sql_read(sql_columns)
# Extract unique fixture dates
fixture_dates = set()
for col in columns:
if col['column_name'].startswith('motm_') or col['column_name'].startswith('dotd_'):
date_part = col['column_name'].split('_')[1]
if date_part != 'total':
fixture_dates.add(date_part)
fixture_dates = sorted(fixture_dates, reverse=True)
# Get current data with dynamic totals
sql_data = text("SELECT * FROM _hkfc_c_motm ORDER BY playernumber")
motm_data = sql_read(sql_data)
# Calculate dynamic totals for each player
for player in motm_data:
motm_total = 0
dotd_total = 0
# Sum all fixture-specific columns
for col_name in player.keys():
if col_name.startswith('motm_') and col_name != 'motmtotal':
motm_total += player[col_name] or 0
elif col_name.startswith('dotd_') and col_name != 'dotdtotal':
dotd_total += player[col_name] or 0
player['calculated_motmtotal'] = motm_total
player['calculated_dotdtotal'] = dotd_total
return render_template('motm_management.html',
fixture_dates=fixture_dates,
motm_data=motm_data)
@app.route('/admin/squad/submit', methods=['POST'])
@basic_auth.required
def match_squad_submit():
@ -1263,12 +1431,34 @@ def vote_results():
@app.route('/api/poty-results')
def poty_results():
"""API endpoint for Player of the Year results"""
sql = text("SELECT playername, motmtotal, dotdtotal FROM _hkfc_c_motm WHERE (motmtotal > 0) OR (dotdtotal > 0)")
print(f"SQL: {sql}")
"""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)
print(f"Results: {rows}")
return jsonify(rows)
# 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')

View File

@ -144,6 +144,14 @@
</a>
</div>
</div>
<div class="col-md-4">
<div class="list-group card-custom">
<a href="/admin/motm/manage" class="list-group-item">
<h4 class="list-group-item-heading">MOTM Management</h4>
<p class="list-group-item-text">Reset MOTM/DotD counts for specific fixtures</p>
</a>
</div>
</div>
<div class="col-md-4">
<div class="list-group card-custom">
<a href="/admin/poty" class="list-group-item">

View File

@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MOTM Management - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.fixture-header {
background-color: #f8f9fa;
font-weight: bold;
}
.reset-section {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 2rem;
}
.data-section {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 2rem;
}
</style>
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<h1>MOTM/DotD Management</h1>
<p class="lead">Manage and reset Man of the Match and Dick of the Day counts</p>
<div class="mb-3">
<a href="/admin" class="btn btn-outline-primary">Back to Admin Dashboard</a>
<a href="/admin/motm" class="btn btn-outline-secondary">MOTM Admin</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Reset Controls -->
<div class="reset-section">
<h3>Reset Controls</h3>
<p class="text-muted">Use these controls to reset MOTM/DotD counts for specific fixtures or all data.</p>
<div class="row">
<div class="col-md-4">
<h5>Reset Specific Fixture</h5>
<form method="POST" onsubmit="return confirm('Are you sure you want to reset MOTM/DotD counts for this fixture?')">
<input type="hidden" name="action" value="reset_fixture">
<div class="mb-3">
<select name="fixture_date" class="form-select" required>
<option value="">Select fixture date...</option>
{% for date in fixture_dates %}
<option value="{{ date }}">{{ date }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-warning">Reset Fixture</button>
</form>
</div>
<div class="col-md-4">
<h5>Reset All Totals</h5>
<p class="text-muted">Reset motmtotal, dotdtotal, assiststotal, goalstotal columns</p>
<form method="POST" onsubmit="return confirm('Are you sure you want to reset all MOTM/DotD totals? This will affect the Player of the Year calculations.')">
<input type="hidden" name="action" value="reset_totals">
<button type="submit" class="btn btn-danger">Reset All Totals</button>
</form>
</div>
<div class="col-md-4">
<h5>Reset Everything</h5>
<p class="text-muted">Reset all MOTM/DotD data including fixture-specific columns</p>
<form method="POST" onsubmit="return confirm('Are you sure you want to reset ALL MOTM/DotD data? This action cannot be undone.')">
<input type="hidden" name="action" value="reset_all">
<button type="submit" class="btn btn-danger">Reset Everything</button>
</form>
</div>
</div>
<div class="row mt-3">
<div class="col-md-4">
<h5>Sync Totals</h5>
<p class="text-muted">Update stored totals to match calculated values from fixture columns</p>
<form method="POST" onsubmit="return confirm('Sync stored totals with calculated values?')">
<input type="hidden" name="action" value="sync_totals">
<button type="submit" class="btn btn-info">Sync Totals</button>
</form>
</div>
</div>
</div>
<!-- Current Data Display -->
<div class="data-section">
<h3>Current MOTM/DotD Data</h3>
{% if motm_data %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Player #</th>
<th>Player Name</th>
<th>MOTM Total</th>
<th>DotD Total</th>
<th>Goals Total</th>
<th>Assists Total</th>
{% for date in fixture_dates %}
<th class="fixture-header">MOTM {{ date }}</th>
<th class="fixture-header">DotD {{ date }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for player in motm_data %}
<tr>
<td>{{ player.playernumber }}</td>
<td>{{ player.playername }}</td>
<td>
<span class="badge bg-primary">{{ player.calculated_motmtotal or 0 }}</span>
{% if player.motmtotal != player.calculated_motmtotal %}
<small class="text-warning">(stored: {{ player.motmtotal or 0 }})</small>
{% endif %}
</td>
<td>
<span class="badge bg-secondary">{{ player.calculated_dotdtotal or 0 }}</span>
{% if player.dotdtotal != player.calculated_dotdtotal %}
<small class="text-warning">(stored: {{ player.dotdtotal or 0 }})</small>
{% endif %}
</td>
<td>
<span class="badge bg-success">{{ player.goalstotal or 0 }}</span>
</td>
<td>
<span class="badge bg-info">{{ player.assiststotal or 0 }}</span>
</td>
{% for date in fixture_dates %}
<td>
{% set motm_col = 'motm_' + date %}
{% set dotd_col = 'dotd_' + date %}
{% if player[motm_col] and player[motm_col] > 0 %}
<span class="badge bg-primary">{{ player[motm_col] }}</span>
<button type="button" class="btn btn-sm btn-outline-danger ms-1"
onclick="resetPlayerFixture({{ player.playernumber }}, '{{ date }}')"
title="Reset this player's counts for {{ date }}">
×
</button>
{% else %}
<span class="text-muted">0</span>
{% endif %}
</td>
<td>
{% if player[dotd_col] and player[dotd_col] > 0 %}
<span class="badge bg-secondary">{{ player[dotd_col] }}</span>
<button type="button" class="btn btn-sm btn-outline-danger ms-1"
onclick="resetPlayerFixture({{ player.playernumber }}, '{{ date }}')"
title="Reset this player's counts for {{ date }}">
×
</button>
{% else %}
<span class="text-muted">0</span>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<h5>No MOTM/DotD data found</h5>
<p>There is no data in the _hkfc_c_motm table. This might be because no votes have been cast yet.</p>
</div>
{% endif %}
</div>
<!-- Column Management -->
<div class="reset-section">
<h3>Column Management</h3>
<p class="text-muted">Drop unwanted columns from the _hkfc_c_motm table.</p>
<div class="row">
<div class="col-md-6">
<h5>Drop Specific Column</h5>
<form method="POST" onsubmit="return confirm('Are you sure you want to drop this column? This action cannot be undone!')">
<input type="hidden" name="action" value="drop_column">
<div class="mb-3">
<select name="column_name" class="form-select" required>
<option value="">Select column to drop...</option>
{% for date in fixture_dates %}
<option value="motm_{{ date }}">motm_{{ date }}</option>
<option value="dotd_{{ date }}">dotd_{{ date }}</option>
{% endfor %}
<option value="motm_none">motm_none</option>
<option value="dotd_none">dotd_none</option>
</select>
</div>
<button type="submit" class="btn btn-danger">Drop Column</button>
</form>
</div>
<div class="col-md-6">
<h5>Drop Fixture Columns</h5>
<form method="POST" onsubmit="return confirm('Are you sure you want to drop all columns for this fixture? This action cannot be undone!')">
<input type="hidden" name="action" value="drop_fixture_columns">
<div class="mb-3">
<select name="fixture_date" class="form-select" required>
<option value="">Select fixture date...</option>
{% for date in fixture_dates %}
<option value="{{ date }}">{{ date }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-danger">Drop Fixture Columns</button>
</form>
</div>
</div>
</div>
<!-- Available Fixtures -->
<div class="data-section">
<h3>Available Fixtures</h3>
{% if fixture_dates %}
<p>The following fixture dates have MOTM/DotD columns in the database:</p>
<ul>
{% for date in fixture_dates %}
<li><code>{{ date }}</code> - Columns: <code>motm_{{ date }}</code>, <code>dotd_{{ date }}</code></li>
{% endfor %}
</ul>
{% else %}
<div class="alert alert-warning">
<h5>No fixture columns found</h5>
<p>No fixture-specific MOTM/DotD columns were found in the database.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Hidden form for individual player resets -->
<form id="playerResetForm" method="POST" style="display: none;">
<input type="hidden" name="action" value="reset_player_fixture">
<input type="hidden" name="player_number" id="playerNumber">
<input type="hidden" name="fixture_date" id="fixtureDate">
</form>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
function resetPlayerFixture(playerNumber, fixtureDate) {
if (confirm(`Are you sure you want to reset MOTM/DotD counts for player #${playerNumber} in fixture ${fixtureDate}?`)) {
document.getElementById('playerNumber').value = playerNumber;
document.getElementById('fixtureDate').value = fixtureDate;
document.getElementById('playerResetForm').submit();
}
}
</script>
</body>
</html>