diff --git a/motm_app/POSTGRESQL_SETUP.md b/motm_app/POSTGRESQL_SETUP.md new file mode 100644 index 0000000..1153063 --- /dev/null +++ b/motm_app/POSTGRESQL_SETUP.md @@ -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. diff --git a/motm_app/forms.py b/motm_app/forms.py index d2163c2..7349032 100644 --- a/motm_app/forms.py +++ b/motm_app/forms.py @@ -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') diff --git a/motm_app/main.py b/motm_app/main.py index 0cda581..7dcd533 100644 --- a/motm_app/main.py +++ b/motm_app/main.py @@ -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 diff --git a/motm_app/run_motm.bat b/motm_app/run_motm.bat index df2fe03..0185432 100644 --- a/motm_app/run_motm.bat +++ b/motm_app/run_motm.bat @@ -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 diff --git a/motm_app/run_motm.sh b/motm_app/run_motm.sh old mode 100644 new mode 100755 index 90c6ab2..696e375 --- a/motm_app/run_motm.sh +++ b/motm_app/run_motm.sh @@ -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 diff --git a/motm_app/static/images/clubs/placeholder_logo.svg b/motm_app/static/images/clubs/placeholder_logo.svg new file mode 100644 index 0000000..1a13741 --- /dev/null +++ b/motm_app/static/images/clubs/placeholder_logo.svg @@ -0,0 +1,4 @@ + diff --git a/motm_app/static/images/default_player.png b/motm_app/static/images/default_player.png new file mode 100644 index 0000000..31d95e8 --- /dev/null +++ b/motm_app/static/images/default_player.png @@ -0,0 +1 @@ +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== diff --git a/motm_app/tables.py b/motm_app/tables.py index 4bba77f..c87d734 100644 --- a/motm_app/tables.py +++ b/motm_app/tables.py @@ -30,11 +30,11 @@ class matchSquadTable: html += '
\n' for item in self.items: html += '