Fix up voting and start adding club logos

This commit is contained in:
Jonny Ervine 2025-10-03 00:13:09 +08:00
parent 7f0d171e21
commit 131f17947c
17 changed files with 516 additions and 37 deletions

View File

@ -0,0 +1,60 @@
# PostgreSQL Database Setup
This application is now configured to use PostgreSQL as the default database instead of SQLite.
## Database Configuration
The application connects to a PostgreSQL database with the following settings:
- **Host**: icarus.ipa.champion
- **Port**: 5432
- **Database**: motm
- **Username**: motm_user
- **Password**: q7y7f7Lv*sODJZ2wGiv0Wq5a
## Running the Application
### Linux/macOS
```bash
./run_motm.sh
```
### Windows
```cmd
run_motm.bat
```
### Manual Setup
If you need to run the application manually, set these environment variables:
```bash
export DATABASE_TYPE=postgresql
export POSTGRES_HOST=icarus.ipa.champion
export POSTGRES_PORT=5432
export POSTGRES_DATABASE=motm
export POSTGRES_USER=motm_user
export POSTGRES_PASSWORD='q7y7f7Lv*sODJZ2wGiv0Wq5a'
```
## Database Status
The PostgreSQL database contains the following data:
- **Players**: 3 players in the `_hkfc_players` table
- **Match Squad**: 3 players currently selected for the match squad
- **Admin Settings**: Configured with next match details
## Troubleshooting
If you encounter issues:
1. **Connection Failed**: Check if the PostgreSQL server is accessible
2. **Empty Data**: The database contains sample data - if you see empty tables, check the connection
3. **Permission Errors**: Ensure the `motm_user` has proper database permissions
## Testing Database Connection
Run the test script to verify everything is working:
```bash
python3 test_match_squad.py
```
This will test the database connection and display current data.

View File

@ -1,6 +1,6 @@
# encoding=utf-8
from flask_wtf import FlaskForm
from wtforms import BooleanField, StringField, PasswordField, IntegerField, TextAreaField, SubmitField, RadioField, SelectField, DateField
from wtforms import BooleanField, StringField, PasswordField, IntegerField, TextAreaField, SubmitField, RadioField, SelectField, DateField, FieldList
from wtforms_components import read_only
from wtforms import validators, ValidationError
from wtforms.validators import InputRequired, Email, Length
@ -106,10 +106,19 @@ class ClubForm(FlaskForm):
class TeamForm(FlaskForm):
"""Form for adding/editing teams."""
club = StringField('Club', validators=[InputRequired()])
club = SelectField('Club', validators=[InputRequired()], choices=[], coerce=str)
team = StringField('Team', validators=[InputRequired()])
display_name = StringField('Display Name', validators=[InputRequired()])
league = StringField('League', validators=[InputRequired()])
league = SelectField('League', validators=[InputRequired()], choices=[
('', 'Select League'),
('Premier Division', 'Premier Division'),
('1st Division', '1st Division'),
('2nd Division', '2nd Division'),
('3rd Division', '3rd Division'),
('4th Division', '4th Division'),
('5th Division', '5th Division'),
('6th Division', '6th Division')
], coerce=str)
# Action buttons
save_team = SubmitField('Save Team')
@ -126,3 +135,16 @@ class DataImportForm(FlaskForm):
# Action buttons
import_data = SubmitField('Import Data')
cancel = SubmitField('Cancel')
class ClubSelectionForm(FlaskForm):
"""Form for selecting which clubs to import."""
# This will be populated dynamically with club checkboxes
selected_clubs = FieldList(BooleanField('Select Club'), min_entries=0)
# Action buttons
import_selected = SubmitField('Import Selected Clubs')
select_all = SubmitField('Select All')
select_none = SubmitField('Select None')
cancel = SubmitField('Cancel')

View File

@ -27,7 +27,7 @@ from flask_basicauth import BasicAuth
from wtforms import StringField, PasswordField, BooleanField
from wtforms import DateField
from wtforms.validators import InputRequired, Email, Length
from forms import motmForm, adminSettingsForm2, goalsAssistsForm, DatabaseSetupForm, PlayerForm, ClubForm, TeamForm, DataImportForm
from forms import motmForm, adminSettingsForm2, goalsAssistsForm, DatabaseSetupForm, PlayerForm, ClubForm, TeamForm, DataImportForm, ClubSelectionForm
from db_config import sql_write, sql_write_static, sql_read, sql_read_static
from sqlalchemy import text
from tables import matchSquadTable
@ -75,6 +75,13 @@ def motm_vote(randomUrlSuffix):
currMotM = nextInfo[0]['currmotm']
currDotD = nextInfo[0]['currdotd']
oppo = nextTeam
# Get opponent club logo from clubs table
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 = club_logo_result[0]['logo_url']
# Get match date from admin settings
if nextInfo and nextInfo[0]['nextdate']:
nextDate = nextInfo[0]['nextdate']
@ -123,14 +130,14 @@ def motm_vote(randomUrlSuffix):
@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")
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')
commentDate = row[0]['nextdate'].strftime('%Y-%m-%d')
_matchDate = row[0]['nextdate'].strftime('%Y_%m_%d')
hkfcLogo = row[0]['hkfcLogo']
oppoLogo = row[0]['oppoLogo']
if request.method == 'POST':
@ -257,7 +264,7 @@ def motm_admin():
# 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 = 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,
@ -267,7 +274,7 @@ def motm_admin():
})
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 = text("UPDATE motmadminsettings SET nextdate = :next_date, nextClub = :next_club, nextTeam = :next_team")
sql_write_static(sql, {
'next_date': _nextMatchDate,
'next_club': _nextClub,
@ -290,7 +297,7 @@ def motm_admin():
print(urlSuffix)
sql3 = text("UPDATE motmadminsettings SET motmurlsuffix = :url_suffix WHERE userid = 'admin'")
sql_write_static(sql3, {'url_suffix': urlSuffix})
flash('MotM URL https://hockey.ervine.cloud/motm/'+urlSuffix)
flash('MotM URL https://motm.ervine.cloud/motm/'+urlSuffix)
elif form.activateButton.data:
# Generate a fixture number based on the date
_nextFixture = _nextMatchDate.replace('-', '')
@ -309,7 +316,7 @@ def motm_admin():
currSuffix = tempSuffix[0]['motmurlsuffix']
print(currSuffix)
flash('Man of the Match vote is now activated')
flash('MotM URL https://hockey.ervine.cloud/motm/'+currSuffix)
flash('MotM URL https://motm.ervine.cloud/motm/'+currSuffix)
else:
flash('Something went wrong - check with Smithers')
@ -554,6 +561,11 @@ 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
@ -586,6 +598,11 @@ 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")
@ -801,6 +818,107 @@ def 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/squad/submit', methods=['POST'])
@basic_auth.required
def match_squad_submit():
@ -1162,7 +1280,7 @@ def voting_chart():
date_result = sql_read_static(sql_date)
if date_result:
matchDate = date_result[0]['nextDate'].replace('-', '')
matchDate = str(date_result[0]['nextdate']).replace('-', '')
else:
matchDate = '20251012' # Default fallback

View File

@ -1,5 +1,16 @@
@echo off
echo 🐍 Starting MOTM Application...
REM Set PostgreSQL environment variables
set DATABASE_TYPE=postgresql
set POSTGRES_HOST=icarus.ipa.champion
set POSTGRES_PORT=5432
set POSTGRES_DATABASE=motm
set POSTGRES_USER=motm_user
set POSTGRES_PASSWORD=q7y7f7Lv*sODJZ2wGiv0Wq5a
echo 📊 Using PostgreSQL database: %POSTGRES_DATABASE% on %POSTGRES_HOST%
call venv\Scripts\activate.bat
python.exe main.py
pause

11
motm_app/run_motm.sh Normal file → Executable file
View File

@ -1,5 +1,16 @@
#!/bin/bash
echo "🐍 Starting MOTM Application..."
# Set PostgreSQL environment variables
export DATABASE_TYPE=postgresql
export POSTGRES_HOST=icarus.ipa.champion
export POSTGRES_PORT=5432
export POSTGRES_DATABASE=motm
export POSTGRES_USER=motm_user
export POSTGRES_PASSWORD='q7y7f7Lv*sODJZ2wGiv0Wq5a'
echo "📊 Using PostgreSQL database: $POSTGRES_DATABASE on $POSTGRES_HOST"
source venv/bin/activate
python main.py

View File

@ -0,0 +1,4 @@
<svg width="120" height="80" xmlns="http://www.w3.org/2000/svg">
<rect width="120" height="80" fill="#f8f9fa" stroke="#dee2e6" stroke-width="2"/>
<text x="60" y="45" font-family="Arial, sans-serif" font-size="12" text-anchor="middle" fill="#6c757d">Club Logo</text>
</svg>

After

Width:  |  Height:  |  Size: 277 B

View File

@ -0,0 +1 @@
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==

View File

@ -30,11 +30,11 @@ class matchSquadTable:
html += ' <tbody>\n'
for item in self.items:
html += ' <tr>\n'
html += f' <td>{item.get("playerNumber", "")}</td>\n'
html += f' <td>{item.get("playerNickname", "")}</td>\n'
html += f' <td>{item.get("playerSurname", "")}</td>\n'
html += f' <td>{item.get("playerForenames", "")}</td>\n'
html += f' <td><form method="post" action="/admin/squad/remove?playerNumber={item.get("playerNumber", "")}"><button type="submit" class="btn btn-danger">Delete</button></form></td>\n'
html += f' <td>{item.get("playernumber", "")}</td>\n'
html += f' <td>{item.get("playernickname", "")}</td>\n'
html += f' <td>{item.get("playersurname", "")}</td>\n'
html += f' <td>{item.get("playerforenames", "")}</td>\n'
html += f' <td><form method="post" action="/admin/squad/remove?playerNumber={item.get("playernumber", "")}"><button type="submit" class="btn btn-danger">Delete</button></form></td>\n'
html += ' </tr>\n'
html += ' </tbody>\n'

View File

@ -43,7 +43,7 @@
<div class="mb-3">
{{ form.logo_url.label(class="form-label") }}
{{ form.logo_url(class="form-control") }}
{{ form.logo_url(class="form-control", id="logoUrl", onchange="previewLogo()") }}
{% if form.logo_url.errors %}
<div class="text-danger">
{% for error in form.logo_url.errors %}
@ -52,6 +52,15 @@
</div>
{% endif %}
<small class="form-text text-muted">Enter the full URL to the club's logo image</small>
<!-- Logo Preview -->
<div id="logoPreview" class="mt-2" style="display: none;">
<label class="form-label small">Preview:</label>
<div class="border rounded p-2 bg-light">
<img id="previewImage" src="" alt="Logo preview" style="max-height: 80px; max-width: 120px;" onerror="this.style.display='none'; document.getElementById('previewError').style.display='block';" onload="document.getElementById('previewError').style.display='none';">
<div id="previewError" class="text-muted small" style="display: none;">Unable to load image</div>
</div>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
@ -70,5 +79,25 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
function previewLogo() {
const urlInput = document.getElementById('logoUrl');
const previewDiv = document.getElementById('logoPreview');
const previewImg = document.getElementById('previewImage');
if (urlInput.value.trim()) {
previewImg.src = urlInput.value;
previewDiv.style.display = 'block';
} else {
previewDiv.style.display = 'none';
}
}
// Preview logo on page load if URL is already filled
document.addEventListener('DOMContentLoaded', function() {
previewLogo();
});
</script>
</body>
</html>

View File

@ -31,7 +31,7 @@
<div class="mb-3">
{{ form.club.label(class="form-label") }}
{{ form.club(class="form-control") }}
{{ form.club(class="form-select") }}
{% if form.club.errors %}
<div class="text-danger">
{% for error in form.club.errors %}
@ -39,7 +39,7 @@
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">Enter the club name (e.g., HKFC, KCC, USRC)</small>
<small class="form-text text-muted">Select the club from the list of available clubs</small>
</div>
<div class="mb-3">
@ -70,7 +70,7 @@
<div class="mb-3">
{{ form.league.label(class="form-label") }}
{{ form.league(class="form-control") }}
{{ form.league(class="form-select") }}
{% if form.league.errors %}
<div class="text-danger">
{% for error in form.league.errors %}
@ -78,7 +78,7 @@
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">Enter the league/division (e.g., Premier Division, Division 1)</small>
<small class="form-text text-muted">Select the league/division from the list</small>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">

View File

@ -77,6 +77,7 @@
<tr>
<th>ID</th>
<th>Club Name</th>
<th>Logo</th>
<th>Logo URL</th>
<th>Actions</th>
</tr>
@ -87,9 +88,20 @@
<td>{{ club.id }}</td>
<td>{{ club.hockey_club }}</td>
<td>
<a href="{{ club.logo_url }}" target="_blank" class="text-decoration-none">
{{ club.logo_url }}
</a>
{% if club.logo_url %}
<img src="{{ club.logo_url }}" alt="{{ club.hockey_club }} logo" style="max-height: 40px; max-width: 60px;" onerror="this.style.display='none'">
{% else %}
<span class="text-muted">No logo</span>
{% endif %}
</td>
<td>
{% if club.logo_url %}
<a href="{{ club.logo_url }}" target="_blank" class="text-decoration-none small">
{{ club.logo_url[:50] }}{% if club.logo_url|length > 50 %}...{% endif %}
</a>
{% else %}
<span class="text-muted">No URL</span>
{% endif %}
</td>
<td>
<a href="/admin/clubs/edit/{{ club.id }}" class="btn btn-sm btn-outline-primary">Edit</a>

View File

@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Select Clubs to Import - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-10">
<div class="card">
<div class="card-header">
<h3>Select Clubs to Import</h3>
<p class="mb-0 text-muted">Choose which clubs you want to import from the Hong Kong Hockey Association</p>
</div>
<div class="card-body">
{% 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 %}
<div class="alert alert-info">
<h5>Club Selection</h5>
<p>Select the clubs you want to import. You can choose all clubs or select specific ones based on your needs.</p>
<p><strong>Note:</strong> Only new clubs will be imported. Existing clubs will be skipped to prevent duplicates.</p>
</div>
{% if clubs %}
<form method="POST" id="clubSelectionForm">
{{ form.hidden_tag() }}
<div class="mb-3">
<div class="d-flex gap-2 mb-3">
<button type="submit" name="select_all" class="btn btn-outline-primary btn-sm" formnovalidate>
Select All
</button>
<button type="submit" name="select_none" class="btn btn-outline-secondary btn-sm" formnovalidate>
Select None
</button>
<span class="text-muted align-self-center">
<span id="selectedCount">0</span> of {{ clubs|length }} clubs selected
</span>
</div>
</div>
<div class="row">
{% for club in clubs %}
<div class="col-md-6 col-lg-4 mb-3">
<div class="card h-100">
<div class="card-body">
<div class="form-check">
<input class="form-check-input club-checkbox"
type="checkbox"
name="selected_clubs"
value="{{ club.name }}"
id="club_{{ loop.index }}"
{% if club.name in selected_clubs %}checked{% endif %}>
<label class="form-check-label w-100" for="club_{{ loop.index }}">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">{{ club.name }}</h6>
{% if club.abbreviation %}
<small class="text-muted">{{ club.abbreviation }}</small>
{% endif %}
</div>
<div class="text-end">
{% if club.teams %}
<small class="badge bg-info">{{ club.teams|length }} teams</small>
{% endif %}
</div>
</div>
{% if club.convenor %}
<div class="mt-2">
<small class="text-muted">
<i class="bi bi-person"></i> {{ club.convenor }}
</small>
</div>
{% endif %}
{% if club.email %}
<div>
<small class="text-muted">
<i class="bi bi-envelope"></i> {{ club.email }}
</small>
</div>
{% endif %}
</label>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-4">
<button type="submit" name="cancel" class="btn btn-secondary me-md-2" formnovalidate>
Cancel
</button>
<button type="submit" name="import_selected" class="btn btn-primary" id="importButton" disabled>
Import Selected Clubs
</button>
</div>
</form>
{% else %}
<div class="alert alert-warning">
<h5>No clubs found</h5>
<p>Unable to fetch clubs from the Hong Kong Hockey Association website. This might be due to:</p>
<ul>
<li>Network connectivity issues</li>
<li>Website structure changes</li>
<li>Server maintenance</li>
</ul>
<p>Please try again later or contact the administrator.</p>
</div>
{% endif %}
</div>
</div>
<div class="mt-3">
<a href="/admin/import" class="btn btn-outline-secondary">Back to Data Import</a>
<a href="/admin" class="btn btn-outline-secondary">Back to Admin</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Update selected count and enable/disable import button
function updateSelection() {
const checkboxes = document.querySelectorAll('.club-checkbox');
const selectedCount = document.querySelectorAll('.club-checkbox:checked').length;
const importButton = document.getElementById('importButton');
const countSpan = document.getElementById('selectedCount');
countSpan.textContent = selectedCount;
importButton.disabled = selectedCount === 0;
}
// Add event listeners to checkboxes
document.addEventListener('DOMContentLoaded', function() {
const checkboxes = document.querySelectorAll('.club-checkbox');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', updateSelection);
});
updateSelection(); // Initial update
});
// Handle select all/none buttons
document.getElementById('clubSelectionForm').addEventListener('submit', function(e) {
if (e.submitter.name === 'select_all') {
e.preventDefault();
document.querySelectorAll('.club-checkbox').forEach(checkbox => {
checkbox.checked = true;
});
updateSelection();
} else if (e.submitter.name === 'select_none') {
e.preventDefault();
document.querySelectorAll('.club-checkbox').forEach(checkbox => {
checkbox.checked = false;
});
updateSelection();
}
});
</script>
</body>
</html>

View File

@ -44,6 +44,14 @@
<small class="form-text text-muted d-block">
Import 30+ hockey clubs including HKFC, KCC, USRC, Valley, SSSC, Dragons, etc.
</small>
<div class="mt-2">
<a href="/admin/import/clubs/select" class="btn btn-outline-primary btn-sm">
Select Specific Clubs
</a>
<small class="text-muted d-block mt-1">
Choose which clubs to import instead of importing all
</small>
</div>
</div>
<div class="form-check mb-3">

View File

@ -43,7 +43,7 @@
<div class="mb-3">
{{ form.logo_url.label(class="form-label") }}
{{ form.logo_url(class="form-control") }}
{{ form.logo_url(class="form-control", id="logoUrl", onchange="previewLogo()") }}
{% if form.logo_url.errors %}
<div class="text-danger">
{% for error in form.logo_url.errors %}
@ -52,6 +52,15 @@
</div>
{% endif %}
<small class="form-text text-muted">Enter the full URL to the club's logo image</small>
<!-- Logo Preview -->
<div id="logoPreview" class="mt-2" style="display: none;">
<label class="form-label small">Preview:</label>
<div class="border rounded p-2 bg-light">
<img id="previewImage" src="" alt="Logo preview" style="max-height: 80px; max-width: 120px;" onerror="this.style.display='none'; document.getElementById('previewError').style.display='block';" onload="document.getElementById('previewError').style.display='none';">
<div id="previewError" class="text-muted small" style="display: none;">Unable to load image</div>
</div>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
@ -70,5 +79,25 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
function previewLogo() {
const urlInput = document.getElementById('logoUrl');
const previewDiv = document.getElementById('logoPreview');
const previewImg = document.getElementById('previewImage');
if (urlInput.value.trim()) {
previewImg.src = urlInput.value;
previewDiv.style.display = 'block';
} else {
previewDiv.style.display = 'none';
}
}
// Preview logo on page load if URL is already filled
document.addEventListener('DOMContentLoaded', function() {
previewLogo();
});
</script>
</body>
</html>

View File

@ -31,7 +31,7 @@
<div class="mb-3">
{{ form.club.label(class="form-label") }}
{{ form.club(class="form-control") }}
{{ form.club(class="form-select") }}
{% if form.club.errors %}
<div class="text-danger">
{% for error in form.club.errors %}
@ -39,7 +39,7 @@
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">Enter the club name (e.g., HKFC, KCC, USRC)</small>
<small class="form-text text-muted">Select the club from the list of available clubs</small>
</div>
<div class="mb-3">
@ -70,7 +70,7 @@
<div class="mb-3">
{{ form.league.label(class="form-label") }}
{{ form.league(class="form-control") }}
{{ form.league(class="form-select") }}
{% if form.league.errors %}
<div class="text-danger">
{% for error in form.league.errors %}
@ -78,7 +78,7 @@
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">Enter the league/division (e.g., Premier Division, Division 1)</small>
<small class="form-text text-muted">Select the league/division from the list</small>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">

View File

@ -30,10 +30,10 @@
<span class="input-group-addon" id="basic-addon1">Man of the Match</span>
<select class="form-control" name="motmVote" required>
{% for item in data %}
{% if item.playerNickname != "" %}
<option value={{ item.playerNumber }}>{{ item.playerNickname }}</option>
{% if item.playernickname != "" %}
<option value={{ item.playernumber }}>{{ item.playernickname }}</option>
{% else %}
<option value={{ item.playerNumber }}>{{ item.playerSurname }}, {{ item.playerForenames }}</option>
<option value={{ item.playernumber }}>{{ item.playersurname }}, {{ item.playerforenames }}</option>
{% endif %}
{% endfor %}
</select>
@ -44,10 +44,10 @@
<span class="input-group-addon" id="basic-addon1">Dick of the Day</span>
<select class="form-control" name="dotdVote" required>
{% for item in data %}
{% if item.playerNickname != "" %}
<option value={{ item.playerNumber }}>{{ item.playerNickname }}</option>
{% if item.playernickname != "" %}
<option value={{ item.playernumber }}>{{ item.playernickname }}</option>
{% else %}
<option value={{ item.playerNumber }}>{{ item.playerSurname }}, {{ item.playerForenames }}</option>
<option value={{ item.playernumber }}>{{ item.playersurname }}, {{ item.playerforenames }}</option>
{% endif %}
{% endfor %}
</select>

View File

@ -11,7 +11,7 @@
<body>
Smithers' army of Internet monkeys will now go about adding up the votes ...
<p>
<img src="https://storage.googleapis.com/hk-hockey-data/images/monkey-typing-simpsons.jpg"></img>
<img src="http://icarus.ipa.champion:9000/hockey-app/assets/simpsons-monkeys.jpg"></img>
</p>
<a class="btn btn-primary" href="/" role="button">Home</a>
<a class="btn btn-info" href="/motm/comments" role="button">Comments</a>