Separate MotM app out

This commit is contained in:
Jonny Ervine 2025-09-29 10:43:48 +08:00
parent 5638b0af76
commit d3eb5567b6
45 changed files with 1621 additions and 52 deletions

View File

@ -78,13 +78,6 @@ class clubPlayingRecordsForm(FlaskForm):
clubName = SelectField("Club to search", choices=[], coerce=str) clubName = SelectField("Club to search", choices=[], coerce=str)
submitButton = SubmitField("Submit") submitButton = SubmitField("Submit")
class motmForm(FlaskForm):
startDate = DateField('DatePicker', format='%d-%m-%Y')
endDate = DateField('DatePicker', format='%d-%m-%Y')
class motmAdminForm(FlaskForm):
startDate = DateField('DatePicker', format='%d-%m-%Y')
endDate = DateField('DatePicker', format='%d-%m-%Y')
class squadListForm(FlaskForm): class squadListForm(FlaskForm):
teamName = SelectField("HKFC team to display") teamName = SelectField("HKFC team to display")
@ -99,28 +92,4 @@ class adminSettingsForm(FlaskForm):
saveButton = SubmitField('Save Settings') saveButton = SubmitField('Save Settings')
activateButton = SubmitField('Activate MotM Vote') activateButton = SubmitField('Activate MotM Vote')
class goalsAssistsForm(FlaskForm):
fixtureNumber = TextField('Fixture Number')
match = SelectField('Fixture')
homeTeam = TextField('Home Team')
awayTeam = TextField('Away Team')
playerNumber = TextField('Player Number')
playerName = TextField('Player Name')
assists = SelectField('Assists:', choices=[(0, '0'), (1, '1'), (2, '2'), (3, '3'), (4, '4')])
goals = SelectField('Goals:', choices=[(0, '0'), (1, '1'), (2, '2'), (3, '3'), (4, '4')])
submit = SubmitField('Submit')
# def __init__(self, *args, **kwargs):
# super(goalsAssistsForm, self).__init__(*args, **kwargs)
# read_only(self.homeTeam)
# read_only(self.awayTeam)
class adminSettingsForm2(FlaskForm):
nextMatch = SelectField('Fixture', choices=[], default=mySettings('match'))
nextOppoClub = TextField('Next Opposition Club:', default=mySettings('club'))
nextOppoTeam = TextField("Next Opposition Team:")
currMotM = SelectField('Current Man of the Match:', choices=[], default=mySettings('motm'))
currDotD = SelectField('Current Dick of the Day:', choices=[], default=mySettings('dotd'))
saveButton = SubmitField('Save Settings')
activateButton = SubmitField('Activate MotM Vote')

15
main.py
View File

@ -20,19 +20,6 @@ from routes import *
app.register_blueprint(routes) app.register_blueprint(routes)
@app.route('/hkfc-d/vote-chart', methods=['GET', 'POST'])
def hkfc_d_vote_chart():
form = LoginForm()
print('Here we are')
if form.validate_on_submit():
sql = "SELECT username FROM hockeyUsers WHERE (username= '" + form.username.data + "')"
print(sql)
rows = sql_read(sql)
print(rows)
return redirect(url_for('/hkfc-d/voting'))
# return '<h1>Something went wrong there</h1>'
return render_template('hkfc-d/login-vote.html', form=form)
@app.route('/login', methods=['GET', 'POST']) @app.route('/login', methods=['GET', 'POST'])
def login(): def login():
@ -44,7 +31,7 @@ def login():
rows = sql_write(sql) rows = sql_write(sql)
print(rows) print(rows)
print(rows[0]) print(rows[0])
return redirect(url_for('/hkfc-d/voting')) return redirect(url_for('dashboard'))
else: else:
return 'Something went wrong' return 'Something went wrong'
# return '<h1>Something went wrong there</h1>' # return '<h1>Something went wrong there</h1>'

102
motm_app/README.md Normal file
View File

@ -0,0 +1,102 @@
# HKFC Men's D Team - MOTM (Man of the Match) System
This is a standalone Flask application for managing Man of the Match and Dick of the Day voting for the HKFC Men's D Team hockey club.
## Features
### Public Section
- **Voting Interface**: Players can vote for MOTM and DotD via secure random URLs
- **Match Comments**: View and add comments from matches
- **Current Holders**: Display current MOTM and DotD holders
### Admin Section
- **Match Management**: Set up upcoming matches, opposition teams, and fixtures
- **Squad Management**: Add/remove players from match squads
- **Statistics**: Record goals and assists for players
- **Voting Results**: View real-time voting charts and results
- **Player of the Year**: Track season-long MOTM/DotD statistics
## Installation
1. Install dependencies:
```bash
pip install -r requirements.txt
```
2. Configure database settings in `db_config.py`
3. Run the application:
```bash
python main.py
```
The application will be available at `http://localhost:5000`
## Database Requirements
The application requires access to the following database tables:
- `_hkfcD_matchSquad` - Current match squad
- `_HKFC_players` - Player database
- `hkfcDAdminSettings` - Admin configuration
- `hockeyFixtures` - Match fixtures
- `_hkfc_d_motm` - MOTM/DotD voting results
- `_motmComments` - Match comments
- `_clubTeams` - Club and team information
- `mensHockeyClubs` - Club logos and information
## API Endpoints
### Public Endpoints
- `/` - Main index page
- `/motm/<randomUrlSuffix>` - Voting page (requires valid URL suffix)
- `/motm/comments` - Match comments
- `/motm/vote-thanks` - Vote submission processing
### Admin Endpoints (Basic Auth Required)
- `/admin/motm` - MOTM administration
- `/admin/squad` - Squad management
- `/admin/squad/submit` - Process squad selection
- `/admin/squad/list` - View current squad
- `/admin/squad/remove` - Remove player from squad
- `/admin/squad/reset` - Reset squad for new match
- `/admin/stats` - Goals and assists administration
- `/admin/voting` - Voting results charts
- `/admin/poty` - Player of the Year charts
### API Endpoints
- `/api/vote-results` - Get voting results as JSON
- `/api/poty-results` - Get Player of the Year results as JSON
- `/admin/api/team/<club>` - Get teams for a club
- `/admin/api/logo/<club>` - Get club logo URL
- `/admin/api/fixture/<fixture>` - Get fixture information
## Security
- Admin sections are protected with HTTP Basic Authentication
- Voting URLs use random suffixes to prevent unauthorized access
- All admin actions require authentication
## Usage
1. **Admin Setup**:
- Access `/admin/motm` to configure upcoming matches
- Use `/admin/squad` to select players for the match
- Activate voting to generate the public voting URL
2. **Player Voting**:
- Share the generated voting URL with players
- Players can vote for MOTM and DotD
- Optional comments can be added
3. **Results**:
- View real-time results at `/admin/voting`
- Track season statistics at `/admin/poty`
## Configuration
Update the following in `db_config.py`:
- Database connection details
- Cloud SQL configuration (if using Google Cloud)
- Local database settings
The application uses the same database as the main hockey results system, so ensure proper database access is configured.

13
motm_app/app.py Normal file
View File

@ -0,0 +1,13 @@
# encoding=utf-8
import random
import string
from flask import Flask
from flask_bootstrap import Bootstrap
app = Flask(__name__)
app.secret_key = "4pFwRNNXs+xQSOEaHrq4iSBwl+mq1UTdRuxqhM+RQpo="
Bootstrap(app)
def randomUrlSuffix(stringLength=6):
lettersAndDigits = string.ascii_letters + string.digits
return ''.join(random.choice(lettersAndDigits) for i in range(stringLength))

24
motm_app/app.yaml Normal file
View File

@ -0,0 +1,24 @@
runtime: python39
env_variables:
CLOUDSQL_CONNECTION_NAME: "hk-hockey:asia-east2:hk-hockey-sql"
CLOUDSQL_USER: "hockeyWrite"
CLOUDSQL_WRITE_USER: "hockeyWrite"
CLOUDSQL_READ_USER: "hockeyRead"
CLOUDSQL_PASSWORD: "P8P1YopMlwg8TxhE"
CLOUDSQL_WRITE_PASSWORD: "1URYcxXXlQ6xOWgj"
CLOUDSQL_READ_PASSWORD: "o4GWrbbkBKy3oR6u"
CLOUDSQL_DATABASE: "20209_hockeyResults"
CLOUDSQL_DATABASE_STATIC: "hockeyResults"
CLOUDSQL_CHARSET: "utf8"
handlers:
- url: /static
static_dir: static
- url: /.*
script: auto
automatic_scaling:
min_instances: 1
max_instances: 10

121
motm_app/db_config.py Normal file
View File

@ -0,0 +1,121 @@
# encoding=utf-8
import pymysql
import os
import json
# These environment variables are configured in app.yaml.
CLOUDSQL_CONNECTION_NAME = "hk-hockey:asia-east2:hk-hockey-sql"
LOCAL_DB_SERVER = "mariadb.db.svc.cluster.local"
CLOUDSQL_USER = "root"
CLOUDSQL_WRITE_USER = "hockeyWrite"
CLOUDSQL_READ_USER = "hockeyRead"
CLOUDSQL_PASSWORD = "P8P1YopMlwg8TxhE"
CLOUDSQL_WRITE_PASSWORD = "1URYcxXXlQ6xOWgj"
CLOUDSQL_READ_PASSWORD = "o4GWrbbkBKy3oR6u"
CLOUDSQL_DATABASE = "20209_hockeyResults"
LOCAL_DATABASE = "hockeyResults2021"
CLOUDSQL_DATABASE_STATIC = "hockeyResults"
CLOUDSQL_CHARSET = "utf8"
def write_cloudsql():
# When deployed to App Engine, the `SERVER_SOFTWARE` environment variable
# will be set to 'Google App Engine/version'.
if os.getenv('SERVER_SOFTWARE', '').startswith('Google App Engine/'):
# Connect using the unix socket located at
# /cloudsql/cloudsql-connection-name.
cloudsql_unix_socket = os.path.join('/cloudsql', CLOUDSQL_CONNECTION_NAME)
db = pymysql.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=CLOUDSQL_DATABASE, charset=CLOUDSQL_CHARSET)
else:
db = pymysql.connect(host=LOCAL_DB_SERVER, user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=LOCAL_DATABASE, charset=CLOUDSQL_CHARSET)
return db
def write_cloudsql_static():
# When deployed to App Engine, the `SERVER_SOFTWARE` environment variable
# will be set to 'Google App Engine/version'.
if os.getenv('SERVER_SOFTWARE', '').startswith('Google App Engine/'):
# Connect using the unix socket located at
# /cloudsql/cloudsql-connection-name.
cloudsql_unix_socket = os.path.join('/cloudsql', CLOUDSQL_CONNECTION_NAME)
db = pymysql.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
else:
db = pymysql.connect(host=LOCAL_DB_SERVER, user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
return db
def read_cloudsql():
# When deployed to App Engine, the `SERVER_SOFTWARE` environment variable
# will be set to 'Google App Engine/version'.
if os.getenv('SERVER_SOFTWARE', '').startswith('Google App Engine/'):
# Connect using the unix socket located at
# /cloudsql/cloudsql-connection-name.
cloudsql_unix_socket = os.path.join('/cloudsql', CLOUDSQL_CONNECTION_NAME)
db = pymysql.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=CLOUDSQL_DATABASE, charset=CLOUDSQL_CHARSET)
else:
db = pymysql.connect(host=LOCAL_DB_SERVER, user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=LOCAL_DATABASE, charset=CLOUDSQL_CHARSET)
return db
def read_cloudsql_static():
# When deployed to App Engine, the `SERVER_SOFTWARE` environment variable
# will be set to 'Google App Engine/version'.
if os.getenv('SERVER_SOFTWARE', '').startswith('Google App Engine/'):
# Connect using the unix socket located at
# /cloudsql/cloudsql-connection-name.
cloudsql_unix_socket = os.path.join('/cloudsql', CLOUDSQL_CONNECTION_NAME)
db = pymysql.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
else:
db = pymysql.connect(host=LOCAL_DB_SERVER, user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
return db
def sql_write(sql_cmd):
try:
db = write_cloudsql()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute(sql_cmd)
db.commit()
except Exception as e:
print(e)
finally:
cursor.close()
db.close()
return db
def sql_write_static(sql_cmd):
try:
db = write_cloudsql_static()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute(sql_cmd)
db.commit()
except Exception as e:
print(e)
finally:
cursor.close()
db.close()
return db
def sql_read(sql_cmd):
try:
db = read_cloudsql()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute(sql_cmd)
rows = cursor.fetchall()
except Exception as e:
print(e)
rows = ''
finally:
cursor.close()
db.close()
return rows
def sql_read_static(sql_cmd):
try:
db = read_cloudsql_static()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute(sql_cmd)
rows = cursor.fetchall()
except Exception as e:
print(e)
rows = ''
finally:
cursor.close()
db.close()
return rows

64
motm_app/deploy.py Normal file
View File

@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""
Deployment script for MOTM Flask application to Google App Engine.
"""
import os
import subprocess
import sys
def deploy_to_app_engine():
"""Deploy the MOTM application to Google App Engine."""
print("🚀 Starting deployment to Google App Engine...")
# Check if gcloud is installed
try:
subprocess.run(['gcloud', '--version'], check=True, capture_output=True)
print("✓ Google Cloud SDK is installed")
except (subprocess.CalledProcessError, FileNotFoundError):
print("❌ Google Cloud SDK not found. Please install it first:")
print(" https://cloud.google.com/sdk/docs/install")
return False
# Check if user is authenticated
try:
subprocess.run(['gcloud', 'auth', 'list', '--filter=status:ACTIVE'], check=True, capture_output=True)
print("✓ Google Cloud authentication verified")
except subprocess.CalledProcessError:
print("❌ Not authenticated with Google Cloud. Run 'gcloud auth login' first")
return False
# Check if app.yaml exists
if not os.path.exists('app.yaml'):
print("❌ app.yaml not found in current directory")
return False
print("✓ app.yaml found")
# Deploy the application
try:
print("📦 Deploying application...")
result = subprocess.run(['gcloud', 'app', 'deploy'], check=True)
print("✅ Deployment completed successfully!")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Deployment failed: {e}")
return False
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == '--deploy':
success = deploy_to_app_engine()
sys.exit(0 if success else 1)
else:
print("MOTM Application Deployment Script")
print("=================================")
print()
print("Usage:")
print(" python deploy.py --deploy # Deploy to Google App Engine")
print()
print("Prerequisites:")
print(" 1. Install Google Cloud SDK")
print(" 2. Run 'gcloud auth login'")
print(" 3. Set your project: 'gcloud config set project YOUR_PROJECT_ID'")
print(" 4. Ensure app.yaml is configured correctly")

40
motm_app/forms.py Normal file
View File

@ -0,0 +1,40 @@
# encoding=utf-8
from flask_wtf import FlaskForm
from wtforms import BooleanField, StringField, PasswordField, TextField, IntegerField, TextAreaField, SubmitField, RadioField, SelectField
from wtforms.fields.html5 import DateField
from wtforms_components import read_only
from wtforms import validators, ValidationError
from wtforms.validators import InputRequired, Email, Length
from datetime import datetime
class motmForm(FlaskForm):
startDate = DateField('DatePicker', format='%d-%m-%Y')
endDate = DateField('DatePicker', format='%d-%m-%Y')
class motmAdminForm(FlaskForm):
startDate = DateField('DatePicker', format='%d-%m-%Y')
endDate = DateField('DatePicker', format='%d-%m-%Y')
class adminSettingsForm2(FlaskForm):
nextMatch = SelectField('Fixture', choices=[])
nextOppoClub = TextField('Next Opposition Club:')
nextOppoTeam = TextField("Next Opposition Team:")
currMotM = SelectField('Current Man of the Match:', choices=[])
currDotD = SelectField('Current Dick of the Day:', choices=[])
saveButton = SubmitField('Save Settings')
activateButton = SubmitField('Activate MotM Vote')
class goalsAssistsForm(FlaskForm):
fixtureNumber = TextField('Fixture Number')
match = SelectField('Fixture')
homeTeam = TextField('Home Team')
awayTeam = TextField('Away Team')
playerNumber = TextField('Player Number')
playerName = TextField('Player Name')
assists = SelectField('Assists:', choices=[(0, '0'), (1, '1'), (2, '2'), (3, '3'), (4, '4')])
goals = SelectField('Goals:', choices=[(0, '0'), (1, '1'), (2, '2'), (3, '3'), (4, '4')])
submit = SubmitField('Submit')

376
motm_app/main.py Normal file
View File

@ -0,0 +1,376 @@
# encoding=utf-8
import pymysql
import os
import json
import hashlib, uuid
import datetime
from datetime import datetime
from app import app, randomUrlSuffix
from flask import Flask, flash, render_template, request, redirect, url_for, jsonify
from flask_wtf import FlaskForm
from flask_bootstrap import Bootstrap
from flask_basicauth import BasicAuth
from wtforms import StringField, PasswordField, BooleanField
from wtforms.fields.html5 import DateField
from wtforms.validators import InputRequired, Email, Length
from forms import motmForm, adminSettingsForm2, goalsAssistsForm
from db_config import sql_write, sql_write_static, sql_read, sql_read_static
from tables import matchSquadTable
from readSettings import mySettings
app.config['BASIC_AUTH_USERNAME'] = 'admin'
app.config['BASIC_AUTH_PASSWORD'] = 'letmein'
basic_auth = BasicAuth(app)
@app.route('/')
def index():
"""Main index page for MOTM system"""
return render_template('index.html')
# ==================== 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 = "SELECT playerNumber, playerForenames, playerSurname, playerNickname FROM _hkfcD_matchSquad ORDER BY RAND()"
sql2 = "SELECT nextClub, nextTeam, nextDate, oppoLogo, hkfcLogo, currMotM, currDotD, nextFixture FROM hkfcDAdminSettings"
rows = sql_read(sql)
nextInfo = sql_read_static(sql2)
nextClub = nextInfo[0]['nextClub']
nextTeam = nextInfo[0]['nextTeam']
nextFixture = nextInfo[0]['nextFixture']
hkfcLogo = nextInfo[0]['hkfcLogo']
oppoLogo = nextInfo[0]['oppoLogo']
currMotM = nextInfo[0]['currMotM']
currDotD = nextInfo[0]['currDotD']
oppo = nextTeam
sql3 = "SELECT hockeyResults2021.hockeyFixtures.date, hockeyResults.hkfcDAdminSettings.nextFixture FROM hockeyResults2021.hockeyFixtures INNER JOIN hockeyResults.hkfcDAdminSettings ON hockeyResults2021.hockeyFixtures.fixtureNumber = hockeyResults.hkfcDAdminSettings.nextFixture"
nextMatchDate = sql_read(sql3)
nextDate = nextMatchDate[0]['date']
formatDate = datetime.strftime(nextDate, '%A, %d %B %Y')
sql3 = "SELECT playerPictureURL FROM _HKFC_players INNER JOIN hockeyResults.hkfcDAdminSettings ON _HKFC_players.playerNumber=hockeyResults.hkfcDAdminSettings.currMotM"
sql4 = "SELECT playerPictureURL FROM _HKFC_players INNER JOIN hockeyResults.hkfcDAdminSettings ON _HKFC_players.playerNumber=hockeyResults.hkfcDAdminSettings.currDotD"
motm = sql_read(sql3)
dotd = sql_read(sql4)
motmURL = motm[0]['playerPictureURL']
dotdURL = dotd[0]['playerPictureURL']
sql5 = "SELECT comment FROM _motmComments INNER JOIN hockeyResults.hkfcDAdminSettings ON _motmComments.matchDate=hockeyResults.hkfcDAdminSettings.nextDate ORDER BY RAND() LIMIT 1"
comment = sql_read(sql5)
if comment == "":
comment = "No comments added yet"
form = motmForm()
sql6 = "SELECT motmUrlSuffix FROM hockeyResults.hkfcDAdminSettings WHERE userid='admin'"
urlSuff = sql_read_static(sql6)
randomSuff = urlSuff[0]['motmUrlSuffix']
print(randomSuff)
if randomSuff == randomUrlSuffix:
return render_template('motm_vote.html', data=rows, comment=comment, formatDate=formatDate, matchNumber=nextFixture, oppo=oppo, hkfcLogo=hkfcLogo, oppoLogo=oppoLogo, dotdURL=dotdURL, motmURL=motmURL, form=form)
else:
return render_template('error.html')
@app.route('/motm/comments', methods=['GET', 'POST'])
def match_comments():
"""Display and allow adding match comments"""
sql = "SELECT nextClub, nextTeam, nextDate, oppoLogo, hkfcLogo FROM hkfcDAdminSettings"
row = sql_read_static(sql)
_oppo = row[0]['nextClub']
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':
_comment = request.form['matchComment']
if _comment != 'Optional comments added here':
_fixed_comment = _comment.replace("'", "\\'")
sql3 = "INSERT INTO _motmComments (matchDate, opposition, comment) VALUES ('" + commentDate + "', '" + _oppo + "', '" + _fixed_comment + "')"
sql_write(sql3)
sql = "SELECT comment FROM _motmComments WHERE matchDate='" + _matchDate + "' ORDER BY RAND()"
comments = sql_read(sql)
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':
sql = "INSERT INTO _hkfc_d_motm (playerNumber, playerName, motmTotal, motm_" + _matchDate + ") SELECT playerNumber, playerNickname, '1', '1' FROM _HKFC_players WHERE playerNumber='" + _motm + "' ON DUPLICATE KEY UPDATE motmTotal = motmTotal + 1, motm_" + _matchDate + " = motm_" + _matchDate + " + 1"
sql2 = "INSERT INTO _hkfc_d_motm (playerNumber, playerName, dotdTotal, dotd_" + _matchDate + ") SELECT playerNumber, playerNickname, '1', '1' FROM _HKFC_players WHERE playerNumber='" + _dotd + "' ON DUPLICATE KEY UPDATE dotdTotal = dotdTotal + 1, dotd_" + _matchDate + " = dotd_" + _matchDate + " + 1"
if _comments == "":
print("No comment")
elif _comments == "Optional comments added here":
print("No comment")
else:
sql3 = "INSERT INTO _motmComments (_matchDate, opposition, comment) VALUES ('" + _matchDate + "', '" + _oppo + "', '" + _fixed_comments + "')"
sql_write(sql3)
sql_write(sql)
sql_write(sql2)
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['nextOppoTeam']
_nextFixture = request.form['nextMatch']
_currMotM = request.form['currMotM']
_currDotD = request.form['currDotD']
sql1 = "SELECT club FROM _clubTeams WHERE displayName='" + _nextTeam + "'"
_nextClubName = sql_read_static(sql1)
_nextClub = _nextClubName[0]['club']
sql = "UPDATE hkfcDAdminSettings SET nextFixture='" + _nextFixture + "', nextClub='" + _nextClub + "', nextTeam='" + _nextTeam + "', currMotM=" + _currMotM + ", currDotD=" + _currDotD + ""
sql_write_static(sql)
sql2 = "UPDATE hkfcDAdminSettings INNER JOIN mensHockeyClubs ON hkfcDAdminSettings.nextClub = mensHockeyClubs.hockeyClub SET hkfcDAdminSettings.oppoLogo = mensHockeyClubs.logoURL WHERE mensHockeyClubs.hockeyClub='" + _nextClub + "'"
sql_write_static(sql2)
if form.saveButton.data:
flash('Settings saved!')
urlSuffix = randomUrlSuffix(8)
print(urlSuffix)
sql3 = "UPDATE hkfcDAdminSettings SET motmUrlSuffix='" + urlSuffix + "' WHERE userid='admin'"
sql_write_static(sql3)
flash('MotM URL https://hockey.ervine.cloud/motm/'+urlSuffix)
elif form.activateButton.data:
sql4 = "ALTER TABLE _hkfc_d_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)
sql5 = "SELECT motmUrlSuffix FROM hkfcDAdminSettings WHERE userid='admin'"
tempSuffix = sql_read_static(sql5)
currSuffix = tempSuffix[0]['motmUrlSuffix']
print(currSuffix)
flash('Man of the Match vote is now activated')
flash('MotM URL https://hockey.ervine.cloud/motm/'+currSuffix)
else:
flash('Something went wrong - check with Smithers')
sql7 = "SELECT date, homeTeam, awayTeam, venue, fixtureNumber FROM hockeyFixtures WHERE homeTeam='HKFC D' OR awayTeam='HKFC D'"
matches = sql_read(sql7)
form.nextMatch.choices = [(match['fixtureNumber'], match['date']) for match in matches]
sql4 = "SELECT hockeyClub FROM mensHockeyClubs ORDER BY hockeyClub"
sql5 = "SELECT nextClub, oppoLogo FROM hkfcDAdminSettings"
sql6 = "SELECT playerNumber, playerForenames, playerSurname FROM _hkfcD_matchSquad_" + prevFixture + " ORDER BY playerForenames"
clubs = sql_read_static(sql4)
settings = sql_read_static(sql5)
players = sql_read(sql6)
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/squad', methods=['GET'])
@basic_auth.required
def match_squad():
"""Admin page for managing match squad"""
sql1 = "SELECT team from _clubTeams WHERE club='HKFC' ORDER BY team"
sql2 = "SELECT playerTeam, playerForenames, playerSurname, playerNickname, playerNumber FROM _HKFC_players"
teams = sql_read(sql1)
players = sql_read(sql2)
return render_template('match_squad.html', teams=teams, players=players)
@app.route('/admin/squad/submit', methods=['POST'])
@basic_auth.required
def match_squad_submit():
"""Process squad selection"""
_playerNumbers = request.form.getlist('playerNumber')
for _playerNumber in _playerNumbers:
sql = "INSERT INTO _hkfcD_matchSquad (playerNumber, playerForenames, playerSurname, playerNickname) SELECT playerNumber, playerForenames, playerSurname, playerNickname FROM _HKFC_players WHERE playerNumber='" + _playerNumber + "'"
sql_write(sql)
sql2 = "SELECT playerNumber, playerForenames, playerSurname, playerNickname FROM _hkfcD_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 = "SELECT playerNumber, playerForenames, playerSurname, playerNickname FROM _hkfcD_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['playerNumber']
sql = "DELETE FROM _hkfcD_matchSquad WHERE playerNumber=" + _playerNumber + ""
sql_write(sql)
return render_template('player_removed.html', number=_playerNumber)
@app.route('/admin/squad/reset')
@basic_auth.required
def matchSquadReset():
"""Reset squad for new match"""
_matchNumber = str(mySettings('fixture'))
print(_matchNumber)
sql1 = "RENAME TABLE _hkfcD_matchSquad TO _hkfcD_matchSquad_" + _matchNumber + ""
sql2 = "CREATE TABLE _hkfcD_matchSquad (playerNumber smallint UNIQUE, playerForenames varchar(50), playerSurname varchar(30), playerNickname varchar(30) NOT NULL, PRIMARY KEY (playerNumber))"
sql3 = "UPDATE hkfcDAdminSettings SET prevFixture='" + _matchNumber + "'"
sql_write(sql1)
sql_write(sql2)
sql_write_static(sql3)
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 D' OR awayTeam='HKFC D'"
matches = sql_read(sql)
form.match.choices = [(match['fixtureNumber'], match['date']) for match in matches]
sql2 = "SELECT playerNumber, playerNickname FROM _hkfcD_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_d_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/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 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 myteams[0]['homeTeam'].startswith("HKFC D"):
nextOppo = myteams[0]['awayTeam']
else:
nextOppo = myteams[0]['homeTeam']
sql2 = "SELECT club FROM _clubTeams WHERE displayName ='" + nextOppo + "'"
clubs = sql_read_static(sql2)
clubName = clubs[0]['club']
sql3 = "SELECT logoUrl FROM mensHockeyClubs WHERE hockeyClub ='" + clubName + "'"
logo = sql_read_static(sql3)
clubLogo = logo[0]['logoUrl']
return jsonify(clubLogo)
@app.route('/api/vote-results')
def vote_results():
"""API endpoint for voting results"""
_matchDate = str(mySettings('fixture'))
print(_matchDate)
sql = "SELECT playerName, motm_" + _matchDate + ", dotd_" + _matchDate + " FROM _hkfc_d_motm WHERE (motm_" + _matchDate + " > '0') OR (dotd_" + _matchDate + " > '0')"
print(sql)
rows = sql_read(sql)
print(rows)
return json.dumps(rows)
@app.route('/api/poty-results')
def poty_results():
"""API endpoint for Player of the Year results"""
sql = "SELECT playerName, motmTotal, dotdTotal FROM _hkfc_d_motm WHERE (motmTotal > '0') OR (dotdTotal > '0')"
print(sql)
rows = sql_read(sql)
return json.dumps(rows)
@app.route('/admin/voting')
@basic_auth.required
def voting_chart():
"""Admin page for viewing voting charts"""
matchDate = mySettings('fixture')
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')
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000, debug=True)

16
motm_app/readSettings.py Normal file
View File

@ -0,0 +1,16 @@
# encoding=utf-8
import pymysql
import os
from db_config import sql_read_static
def mySettings(setting):
try:
sql = "SELECT " + setting + " FROM hkfcDAdminSettings WHERE userid='admin'"
rows = sql_read_static(sql)
if rows:
return rows[0][setting]
else:
return None
except Exception as e:
print(e)
return None

11
motm_app/requirements.txt Normal file
View File

@ -0,0 +1,11 @@
Flask
Werkzeug
email-validator
flask_table
flask-mysql
flask_login
Flask-BasicAuth
Flask-Bootstrap
flask_wtf
wtforms_components
pymysql

File diff suppressed because one or more lines are too long

6
motm_app/static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

7
motm_app/static/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

9
motm_app/tables.py Normal file
View File

@ -0,0 +1,9 @@
from flask_table import Table, Col, LinkCol, ButtonCol
class matchSquadTable(Table):
playerNumber = Col('Player Number')
playerNickname = Col('Nickname')
playerSurname = Col('Surname')
playerForenames = Col('Forenames')
delete = ButtonCol('Delete', 'motm.delPlayerFromSquad', url_kwargs=dict(playerNumber='playerNumber'), button_attrs={"type" : "submit", "class" : "btn btn-danger"})

View File

@ -0,0 +1,19 @@
<html>
<head>
<title>Error - Invalid URL</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Error</h1>
<p>Invalid voting URL. Please check the link and try again.</p>
<a class="btn btn-primary" href="/" role="button">Home</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,69 @@
<html>
<head>
<title>Goals and Assists Admin</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<body>
<h3>Goals and Assists Administration</h3>
<form method="post" action="/admin/stats/submit">
{{ form.csrf_token }}
<div class="row">
<div class="col-sm-6">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Match:</span>
{{ form.match(class_="form-control") }}
</div>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<thead>
<tr>
<th>Player Name</th>
<th>Goals</th>
<th>Assists</th>
</tr>
</thead>
<tbody>
{% for player in data %}
<tr>
<td>
<input type="hidden" name="playerName" value="{{ player.playerNickname }}">
<input type="hidden" name="playerNumber" value="{{ player.playerNumber }}">
{{ player.playerNickname }}
</td>
<td>
<select name="goals" class="form-control">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</td>
<td>
<select name="assists" class="form-control">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<button type="submit" class="btn btn-success">Submit</button>
<a class="btn btn-danger" href="/" role="button">Cancel</a>
</form>
</body>
</html>

View File

@ -0,0 +1,19 @@
<html>
<head>
<title>Goals and Assists Submitted</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h3>Goals and Assists have been recorded</h3>
<a class="btn btn-primary" href="/admin/stats" role="button">Add More Stats</a>
<a class="btn btn-danger" href="/" role="button">Home</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,65 @@
<html>
<head>
<title>HKFC Men's D Team - MOTM System</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>HKFC Men's D Team - Man of the Match System</h1>
<div class="jumbotron">
<h2>Welcome to the MOTM Voting System</h2>
<p>This system allows players to vote for Man of the Match and Dick of the Day, while providing admin tools for managing matches and squads.</p>
</div>
<div class="row">
<div class="col-md-6">
<h3>Player Section</h3>
<div class="list-group">
<a href="/motm/comments" class="list-group-item">
<h4 class="list-group-item-heading">Match Comments</h4>
<p class="list-group-item-text">View comments from recent matches</p>
</a>
</div>
</div>
<div class="col-md-6">
<h3>Admin Section</h3>
<div class="list-group">
<a href="/admin/motm" class="list-group-item">
<h4 class="list-group-item-heading">MOTM Admin</h4>
<p class="list-group-item-text">Manage match settings and activate voting</p>
</a>
<a href="/admin/squad" class="list-group-item">
<h4 class="list-group-item-heading">Squad Management</h4>
<p class="list-group-item-text">Add players to match squads</p>
</a>
<a href="/admin/squad/list" class="list-group-item">
<h4 class="list-group-item-heading">View Squad</h4>
<p class="list-group-item-text">View current match squad</p>
</a>
<a href="/admin/stats" class="list-group-item">
<h4 class="list-group-item-heading">Goals & Assists</h4>
<p class="list-group-item-text">Record goals and assists statistics</p>
</a>
<a href="/admin/voting" class="list-group-item">
<h4 class="list-group-item-heading">Voting Results</h4>
<p class="list-group-item-text">View current match voting results</p>
</a>
<a href="/admin/poty" class="list-group-item">
<h4 class="list-group-item-heading">Player of the Year</h4>
<p class="list-group-item-text">View season totals and Player of the Year standings</p>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,31 @@
<html>
<head>
<title>HKFC Men's D Team - Match Comments</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<body>
<h3>Match Comments</h3>
<div class="row">
<div class="col-sm-6">
<img src="{{ hkfcLogo }}" height="100"></img>
<img src="{{ oppoLogo }}" height="100"></img>
</div>
</div>
<div class="row">
<div class="col-sm-8">
{% for comment in comments %}
<div class="panel panel-default">
<div class="panel-body">
{{ comment.comment }}
</div>
</div>
{% endfor %}
</div>
</div>
<a class="btn btn-primary" href="/" role="button">Home</a>
</body>
</html>

View File

@ -0,0 +1,200 @@
<html>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://fonts.googleapis.com/css?family=Raleway" rel="stylesheet">
<style>
* {
box-sizing: border-box;
}
body {
background-color: #f1f1f1;
}
#squadForm {
background-color: #ffffff;
margin: 100px auto;
font-family: Raleway;
padding: 40px;
width: 40%;
min-width: 300px;
}
h1 {
text-align: center;
}
input {
padding: 10px;
width: 10%;
font-size: 17px;
font-family: Raleway;
border: 1px solid #aaaaaa;
}
/* Mark input boxes that gets an error on validation: */
input.invalid {
background-color: #ffdddd;
}
/* Hide all steps by default: */
.tab {
display: none;
}
button {
background-color: #4CAF50;
color: #ffffff;
border: none;
padding: 10px 20px;
font-size: 17px;
font-family: Raleway;
cursor: pointer;
}
button:hover {
opacity: 0.8;
}
#prevBtn {
background-color: #bbbbbb;
}
/* Make circles that indicate the steps of the form: */
.step {
height: 15px;
width: 15px;
margin: 0 2px;
background-color: #bbbbbb;
border: none;
border-radius: 50%;
display: inline-block;
opacity: 0.5;
}
.step.active {
opacity: 1;
}
/* Mark the steps that are finished and valid: */
.step.finish {
background-color: #4CAF50;
}
</style>
<body>
<form id="squadForm" action="/admin/squad/submit" method="post">
<h1>Add Players to Squad</h1>
<!-- One "tab" for each step in the form: -->
{% for team in teams %}
<div class="tab">{{ team.team }} Team Players
<p>
{% for item in players %}
{% if item.playerTeam == team.team %}
<label><input type="checkbox" name="playerNumber" value="{{ item.playerNumber }}">{{ item.playerNumber }} {{ item.playerForenames }} {{ item.playerSurname }}</label>
<br/>
{% endif %}
{% endfor %}
</p>
</div>
{% endfor %}
<div style="overflow:auto;">
<div style="float:right;">
<button type="button" id="prevBtn" onclick="nextPrev(-1)">Previous</button>
<button type="button" id="nextBtn" onclick="nextPrev(1)">Next</button>
</div>
</div>
<!-- Circles which indicates the steps of the form: -->
<div style="text-align:center;margin-top:40px;">
<span class="step"></span>
<span class="step"></span>
<span class="step"></span>
<span class="step"></span>
<span class="step"></span>
<span class="step"></span>
<span class="step"></span>
<span class="step"></span>
</div>
</form>
<script>
var currentTab = 0; // Current tab is set to be the first tab (0)
showTab(currentTab); // Display the crurrent tab
function showTab(n) {
// This function will display the specified tab of the form...
var x = document.getElementsByClassName("tab");
x[n].style.display = "block";
//... and fix the Previous/Next buttons:
if (n == 0) {
document.getElementById("prevBtn").style.display = "none";
} else {
document.getElementById("prevBtn").style.display = "inline";
}
if (n == (x.length - 1)) {
document.getElementById("nextBtn").innerHTML = "Submit";
} else {
document.getElementById("nextBtn").innerHTML = "Next";
}
//... and run a function that will display the correct step indicator:
fixStepIndicator(n)
}
function nextPrev(n) {
// This function will figure out which tab to display
var x = document.getElementsByClassName("tab");
// Exit the function if any field in the current tab is invalid:
// JDE ** Need to validate for this to work? ** if (n == 1 && !validateForm()) return false;
// Hide the current tab:
x[currentTab].style.display = "none";
// Increase or decrease the current tab by 1:
currentTab = currentTab + n;
// if you have reached the end of the form...
if (currentTab >= x.length) {
// ... the form gets submitted:
document.getElementById("squadForm").submit();
return false;
}
// Otherwise, display the correct tab:
showTab(currentTab);
}
function validateForm() {
// This function deals with validation of the form fields
var x, y, i, valid = true;
x = document.getElementsByClassName("tab");
y = x[currentTab].getElementsByTagName("input");
// A loop that checks every input field in the current tab:
for (i = 0; i < y.length; i++) {
// If a field is empty...
if (y[i].value == "") {
// add an "invalid" class to the field:
y[i].className += " invalid";
// and set the current valid status to false
valid = false;
}
}
// If the valid status is true, mark the step as finished and valid:
if (valid) {
document.getElementsByClassName("step")[currentTab].className += " finish";
}
return valid; // return the valid status
}
function fixStepIndicator(n) {
// This function removes the "active" class of all steps...
var i, x = document.getElementsByClassName("step");
for (i = 0; i < x.length; i++) {
x[i].className = x[i].className.replace(" active", "");
}
//... and adds the "active" class on the current step:
x[n].className += " active";
}
</script>
</body>
</html>

View File

@ -0,0 +1,19 @@
<html>
<head>
<title>Squad Reset</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h3>Match squad has been reset for the next match</h3>
<a class="btn btn-primary" href="/admin/squad" role="button">Add Players to New Squad</a>
<a class="btn btn-danger" href="/" role="button">Home</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,24 @@
<html>
<head>
<title>HKFC Men's D Team - Match Squad Selected</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<body>
<h3>Match Squad</h3>
<div class="container">
<div class="row">
<div class="col-md-12">
{{ table }}
</div>
</div>
</div>
<a class="btn btn-primary" href="/admin/squad" role="button">Add More Players</a>
<a class="btn btn-info" href="/admin/squad/list" role="button">View Squad List</a>
<a class="btn btn-warning" href="/admin/squad/reset" role="button">Reset Squad</a>
<a class="btn btn-danger" href="/" role="button">Cancel</a>
</body>
</html>

View File

@ -0,0 +1,105 @@
<html>
<head>
<title>HKFC Men's D Team - MotM and DotD vote admin</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<h2>HKFC Men's D Team MotM and DotD online vote admin page</h2>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<body onload="myFunction()">
<dl>
<p>
{{ form.csrf_token }}
<b>HKFC D Next Opponent:</b>
<br/>
<div class="row">
<div class="col-xs-12">
<form class="col-sm-6" method="post" action="/admin/motm">
<div class = "row">
<div class = "col-sm-6">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Date:</span>
{{ form.nextMatch(class_="form-control") }}
</div>
</div>
</div>
</br>
<div class = "row">
<div class = "col-sm-9">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Opposition</span>
{{ form.nextOppoTeam(class_="form-control") }}
</div>
</div>
</div>
<div class = "row">
<div class = "col-sm-6">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Current Man of the Match:</span>
{{ form.currMotM(class_="form-control") }}
</div>
</div>
<div class = "col-sm-6">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Current Dick of the Day:</span>
{{ form.currDotD(class_="form-control") }}
</div>
</div>
</div>
<p>
{{ form.saveButton(class_="btn btn-success") }}
{{ form.activateButton(class_="btn btn-primary") }}
<a class="btn btn-danger" href="/" role="button">Cancel</a>
</p>
</form>
<div class="col-sm-4">
<img src="{{ nextOppoLogo }}" height="90" id="nextOppoLogo"/>
</div>
</div>
</div>
</p>
</dl>
<script>
var match_select = document.getElementById("nextMatch");
var club_logo = document.getElementById("nextOppoLogo");
var team_select = document.getElementById("nextOppoTeam");
match_select.onchange = function() {myFunction()};
function myFunction() {
match = match_select.value;
fetch('/admin/api/fixture/' + match).then(function(response) {
response.json().then(function(team) {
var optionHTML = '';
optionHTML += team;
team_select.value = optionHTML;
})
});
fetch('/admin/api/fixture/' + match + '/logo').then(function(response) {
response.json().then(function(logo) {
var HTML = '';
HTML += logo;
club_logo.src = HTML;
})
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,83 @@
<html>
<head>
<title>HKFC Men's D Team - MotM and DotD online vote</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<h3>HKFC Men's D Team MotM and DotD online vote</h3>
<h5>{{ formatDate }}</h5>
<h4><img src="{{ hkfcLogo }}" height="150"></img> <b> </b> <img src="{{ oppoLogo }}" height="140"></img></h4>
<body>
<p><b>Randomly selected comment from the match:</b>
<br/>
{% for item in comment %}
<i>{{ item.comment }}</i>
{% endfor %}
</p>
<dl>
{{ form.csrf_token }}
<div class="row">
<div class="col-xs-12">
<form class="col-sm-6" method="post" action="/motm/vote-thanks" id="motmForm" accept-charset="utf-8">
<input type="hidden" id="matchNumber" name="matchNumber" value="{{ matchNumber }}">
<input type="hidden" id="oppo" name="oppo" value="{{ oppo }}">
<div class = "row">
<div class = "col-sm-6">
<div class="input-group">
<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>
{% else %}
<option value={{ item.playerNumber }}>{{ item.playerSurname }}, {{ item.playerForenames }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class = "col-sm-6">
<div class="input-group">
<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>
{% else %}
<option value={{ item.playerNumber }}>{{ item.playerSurname }}, {{ item.playerForenames }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
</div>
<div class = "row">
<div class = "col-sm-6">
<div class = "input-group">
<span class = "input-group-addon" id = "basic-addon1">Match comments</span>
<textarea rows = "4" cols = "80" name = "motmComment" form = "motmForm">Optional comments added here</textarea>
</div>
</div>
</div>
<div class = "row">
<h3>Rogues Gallery</h3>
<div class = "col-sm-4">
<h4>Current Man of the Match</h4>
<img src="{{ motmURL }}" height="200"></img>
</div>
<div class = "col-sm-4">
<h4>Current Dick of the Day</h4>
<img src="{{ dotdURL }}" height="200"></img>
</div>
</div>
<button type="submit" class="btn btn-success">Submit</button>
<a class="btn btn-danger" href="/" role="button">Cancel</a>
</form>
</div>
</div>
</dl>
</body>
</html>

View File

@ -0,0 +1,19 @@
<html>
<head>
<title>Player Removed</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h3>Player {{ number }} has been removed from the squad</h3>
<a class="btn btn-primary" href="/admin/squad/list" role="button">Back to Squad List</a>
<a class="btn btn-danger" href="/" role="button">Home</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,56 @@
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="chart_div" style="width: 800px; height: 1000px;"></div>
<script type="text/javascript">
google.charts.load('current', {
packages: ['corechart']
}).then(function () {
// create chart
var container = $('#chart_div').get(0);
var chart = new google.visualization.ColumnChart(container);
var options = {
legend: {
position: 'top'
}
};
// create data table
var data = new google.visualization.DataTable();
data.addColumn('string', 'Player');
data.addColumn('number', 'MotM Total');
data.addColumn('number', 'DotD Total');
// get data
$.ajax({
url: '/api/poty-results',
dataType: 'json'
}).done(function (jsonData) {
loadData(jsonData);
}).fail(function (jqXHR, textStatus, errorThrown) {
var jsonData = [{"motmTotal": 5, "playerName": "ERVINE Jonathan Desmond", "dotdTotal": 2}, {"motmTotal": 3, "playerName": "MCDONAGH Jerome Michael", "dotdTotal": 1}];
loadData(jsonData);
});
// load json data
function loadData(jsonData) {
$.each(jsonData, function(index, row) {
data.addRow([
row.playerName,
row.motmTotal,
row.dotdTotal
]);
});
drawChart();
}
// draw chart
$(window).resize(drawChart);
function drawChart() {
chart.draw(data, options);
}
});
</script>

View File

@ -0,0 +1,56 @@
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="chart_div" style="width: 800px; height: 1000px;"></div>
<script type="text/javascript">
google.charts.load('current', {
packages: ['corechart']
}).then(function () {
// create chart
var container = $('#chart_div').get(0);
var chart = new google.visualization.ColumnChart(container);
var options = {
legend: {
position: 'top'
}
};
// create data table
var data = new google.visualization.DataTable();
data.addColumn('string', 'Player');
data.addColumn('number', 'MotM');
data.addColumn('number', 'DotD');
// get data
$.ajax({
url: '/api/vote-results',
dataType: 'json'
}).done(function (jsonData) {
loadData(jsonData);
}).fail(function (jqXHR, textStatus, errorThrown) {
var jsonData = [{"motm_{{ _matchDate }}": 1, "playerName": "ERVINE Jonathan Desmond", "dotd_{{ _matchDate }}": 0}, {"motm_{{ _matchDate }}": 0, "playerName": "MCDONAGH Jerome Michael", "dotd_{{ _matchDate }}": 1}];
loadData(jsonData);
});
// load json data
function loadData(jsonData) {
$.each(jsonData, function(index, row) {
data.addRow([
row.playerName,
row.motm_{{ _matchDate }},
row.dotd_{{ _matchDate }}
]);
});
drawChart();
}
// draw chart
$(window).resize(drawChart);
function drawChart() {
chart.draw(data, options);
}
});
</script>

View File

@ -0,0 +1,19 @@
<html>
<head>
<title>HKFC Men's D Team - MotM and DotD vote</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<h2>Thanks for submitting the MotM and DotD votes</h2>
<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>
</p>
<a class="btn btn-primary" href="/" role="button">Home</a>
<a class="btn btn-info" href="/motm/comments" role="button">Comments</a>
</body>
</html>

41
motm_app/test_app.py Normal file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""
Simple test script to verify the MOTM Flask application can start without errors.
"""
import sys
import os
# Add the current directory to Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
from main import app
print("✓ MOTM Flask application imported successfully")
# Test that we can create the app context
with app.app_context():
print("✓ Flask application context created successfully")
# Test that routes are registered
routes = [rule.rule for rule in app.url_map.iter_rules()]
print(f"✓ Found {len(routes)} registered routes")
# Check for key routes
key_routes = ['/', '/motm/', '/admin/motm', '/admin/squad']
for route in key_routes:
if any(route in r for r in routes):
print(f"✓ Key route {route} is registered")
else:
print(f"⚠ Key route {route} not found")
print("\n🎉 MOTM application test completed successfully!")
print("You can now run 'python main.py' to start the application.")
except ImportError as e:
print(f"❌ Import error: {e}")
print("Make sure all dependencies are installed: pip install -r requirements.txt")
sys.exit(1)
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)

View File

@ -3,5 +3,4 @@ routes = Blueprint('routes', __name__)
from .dashboard import * from .dashboard import *
from ._matches import * from ._matches import *
from ._hkfcD_motm import *
from ._convenor import * from ._convenor import *

View File

@ -68,12 +68,6 @@ class convenorSquadListTable(Table):
edit = ButtonCol('Edit', 'routes.convenorEditPlayer', url_kwargs=dict(playerNumber='playerNumber'), button_attrs={"type" : "submit", "class" : "btn btn-primary"}) edit = ButtonCol('Edit', 'routes.convenorEditPlayer', url_kwargs=dict(playerNumber='playerNumber'), button_attrs={"type" : "submit", "class" : "btn btn-primary"})
delete = ButtonCol('Delete', 'routes.convenorDeletePlayer', url_kwargs=dict(playerNumber='playerNumber'), button_attrs={"type" : "submit", "class" : "btn btn-danger"}) delete = ButtonCol('Delete', 'routes.convenorDeletePlayer', url_kwargs=dict(playerNumber='playerNumber'), button_attrs={"type" : "submit", "class" : "btn btn-danger"})
class matchSquadTable(Table):
playerNumber = Col('Player Number')
playerNickname = Col('Nickname')
playerSurname = Col('Surname')
playerForenames = Col('Forenames')
delete = ButtonCol('Delete', 'routes.delPlayerFromSquad', url_kwargs=dict(playerNumber='playerNumber'), button_attrs={"type" : "submit", "class" : "btn btn-danger"})
class convenorFixtureList(Table): class convenorFixtureList(Table):
date = Col('Date') date = Col('Date')