diff --git a/.flaskenv b/.flaskenv index 85b6b69..d0218b1 100644 --- a/.flaskenv +++ b/.flaskenv @@ -1,2 +1,2 @@ FLASK_APP=trivia.create_app -FLASK_ENV=development \ No newline at end of file +FLASK_ENV=production \ No newline at end of file diff --git a/README.md b/README.md index cae2767..59623a8 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,11 @@ on at `localhost:5000` unless git clone https://github.com/Xevion/trivia.git cd trivia pip install -r requirements.txt -python wsgi.py +flask run ``` +Edit .flaskenv to change the configuration preset. + ## Application Design The webapp has two sides: Client and Server. diff --git a/cli.py b/cli.py index fb29939..bcf298a 100644 --- a/cli.py +++ b/cli.py @@ -5,7 +5,6 @@ A simple CLI implementation using the application's API. """ import curses -import time from datetime import datetime from typing import List @@ -18,7 +17,7 @@ lastAttempt: float = -1 lastUpdate: float = -1 -def refreshScores() -> bool: +def refreshScores() -> None: """ Refreshes scoreboard data safely, handling a unresponsive or downed scoreboard. Uses If-Modified-Since headers properly. @@ -30,21 +29,24 @@ def refreshScores() -> bool: global lastUpdate, lastAttempt, scores # Send with If-Modified-Since header if this is not the first time - headers = {'If-Modified-Since': datetime.fromtimestamp(lastAttempt).strftime('%a, %d %b %Y %I:%M:%S')} if lastAttempt > 0 else {} + useTime = max(lastAttempt, lastUpdate) + headers = { + 'If-Modified-Since': datetime.fromtimestamp(useTime, pytz.utc).strftime( + '%a, %d %b %Y %I:%M:%S %Z')} if useTime > 0 else {} # Send request with headers try: resp = requests.get('http://127.0.0.1:5000/api/scores/', headers=headers) except requests.exceptions.ConnectionError: resp = None finally: - lastAttempt = time.time() + lastAttempt = datetime.utcnow().timestamp() if resp is not None and resp.ok: if resp.status_code == 304 and len(scores) != 0: pass else: # Changes found, update! - lastUpdate = time.time() + lastUpdate = datetime.utcnow().timestamp() scores = resp.json() # Calculate totals, preliminary sort by total @@ -54,25 +56,25 @@ def refreshScores() -> bool: # Calculate ranks with tie handling logic for i, team in enumerate(scores): + # Check that previous score is the same, if so add a 'T' for tie if i > 0 and scores[i - 1]['total'] == team['total']: team['rank'] = scores[i - 1]['rank'] + # Check if we have a T if not team['rank'].startswith('T'): team['rank'] = 'T' + team['rank'].strip() + # Check if previous score has a T if not scores[i - 1]['rank'].startswith('T'): scores[i - 1]['rank'] = 'T' + scores[i - 1]['rank'].strip() - else: + # Otherwise just add a space in front instead team['rank'] = " " + str(i + 1) - return True - return False - def main(screen) -> None: """ - Mainloop function + Mainloop function. :param screen: Curses screen """ @@ -80,7 +82,7 @@ def main(screen) -> None: screen.redrawwin() while True: # Refresh scores every 10 seconds - if time.time() - lastAttempt > 0.5: + if datetime.utcnow().timestamp() - lastAttempt > 0.5: refreshScores() # Get current terminal size and clear @@ -90,6 +92,7 @@ def main(screen) -> None: # Build table data global scores table = [[team['rank'], team['id'], team['name'], team['total'], *team['scores']] for team in scores[:y - 4]] + # Round number headers scoreSet = map(str, range(1, max(8, len(scores[0]['scores'])) + 1)) if scores else [] table.insert(0, ['Rank', 'ID', 'Team Name', 'T', *scoreSet]) table = SingleTable(table, title='EfTA Trivia Night') @@ -104,7 +107,8 @@ def main(screen) -> None: # Terminal Size strpos = str((x, y)) screen.addstr(y - 1, 1, strpos) - screen.addstr(y - 1, 1 + len(strpos) + 1, f'({str(round(0.5 - (time.time() - lastAttempt), 3)).zfill(3)})') + screen.addstr(y - 1, 1 + len(strpos) + 1, + f'({str(round(0.5 - (datetime.utcnow().timestamp() - lastAttempt), 3)).zfill(3)})') # Update curses screen screen.refresh() diff --git a/trivia/api.py b/trivia/api.py index 0afd479..a981b08 100644 --- a/trivia/api.py +++ b/trivia/api.py @@ -27,7 +27,14 @@ def scores(): try: if request.headers['If-Modified-Since']: # Acquire epoch time from header - epoch = time.mktime(time.strptime(request.headers['If-Modified-Since'], "%a, %d %b %Y %I:%M:%S")) + if current_app.config['DEBUG']: + print(request.headers['If-Modified-Since']) + epoch = datetime.strptime(request.headers['If-Modified-Since'], "%a, %d %b %Y %I:%M:%S %Z") + if current_app.config['DEBUG']: + print(epoch) + epoch = epoch.timestamp() + if current_app.config['DEBUG']: + print(epoch, lastChange, lastChange - epoch) if epoch >= lastChange: status_code = 304 except KeyError: diff --git a/trivia/config.py b/trivia/config.py index 8f7c35c..210e123 100644 --- a/trivia/config.py +++ b/trivia/config.py @@ -6,6 +6,7 @@ config.py configs = { 'production': 'trivia.config.Config', + 'productiondep': 'trivia.config.ConfigDeprecated', 'development': 'trivia.config.Config', 'demo': 'trivia.config.DemoConfig' } @@ -15,6 +16,8 @@ class Config(object): # Main Configuration SCORE_FILE = 'scores.json' POLLING_INTERVAL = 5 + DEBUG = False + CONVERT_OLD = True # Demo Configuration DEMO = False @@ -23,9 +26,14 @@ class Config(object): DEMO_MAX_SCORES = 0 +def ConfigDeprecated(Config): + CONVERT_OLD = True + + class DemoConfig(Config): # Main Configuration SCORE_FILE = 'demo.json' + DEBUG = True # Demo Configuration DEMO = True diff --git a/trivia/create_app.py b/trivia/create_app.py index b465566..d066dde 100644 --- a/trivia/create_app.py +++ b/trivia/create_app.py @@ -1,10 +1,11 @@ -from flask_apscheduler import APScheduler from flask import Flask +from flask_apscheduler import APScheduler from trivia.config import configs scheduler: APScheduler = None + def create_app(env=None): app = Flask(__name__) @@ -23,14 +24,16 @@ def create_app(env=None): scheduler.start() # Add score file polling - scheduler.add_job(id='polling', func=utils.refreshScores, trigger="interval", seconds=app.config['POLLING_INTERVAL']) + scheduler.add_job(id='polling', func=utils.refreshScores, trigger="interval", + seconds=app.config['POLLING_INTERVAL']) if app.config['DEMO']: app.logger.info('Generating Demo Data...') # Generate initial Demo data utils.generateDemo() # Begin altering demo data regularly - scheduler.add_job(id='altering', func=utils.alterDemo, trigger="interval", seconds=app.config['DEMO_ALTERATION_INTERVAL']) + scheduler.add_job(id='altering', func=utils.alterDemo, trigger="interval", + seconds=app.config['DEMO_ALTERATION_INTERVAL']) utils.refreshScores() diff --git a/trivia/templates/index.html b/trivia/templates/index.html index dd30735..39c0d03 100644 --- a/trivia/templates/index.html +++ b/trivia/templates/index.html @@ -86,7 +86,11 @@ {% for team in teams %} {{ range(1, teams | length) | random }} - {{ team.name }} + {% if team.name | length > 0 %} + {{ team.name }} + {% else %} + Team {{ team.id }} + {% endif %} {% for score in team.scores %} {{ score }} {% endfor %} diff --git a/trivia/utils.py b/trivia/utils.py index 87e9a55..6ebc9e0 100644 --- a/trivia/utils.py +++ b/trivia/utils.py @@ -5,9 +5,10 @@ Stores important backend application functionality. """ import json import os -import time import random +import time from collections import namedtuple +from datetime import datetime from typing import List # Simple fake 'class' for passing to jinja templates @@ -30,23 +31,22 @@ def lastModified() -> float: """ returns epoch time of last modification to the scores file. """ - return os.stat(SCORES_FILE).st_mtime + if current_app.config['DEBUG']: + print(datetime.fromtimestamp(os.path.getmtime(SCORES_FILE)), datetime.fromtimestamp(time.time())) + return os.path.getmtime(SCORES_FILE) def refreshScores() -> None: """ Refreshes scores data safely. - - :return: """ - global lastChange - curChange = lastModified() - from trivia.create_app import scheduler app = scheduler.app - with app.app_context(): + global lastChange + curChange = lastModified() + if lastChange < curChange: try: # Update tracking var @@ -55,6 +55,8 @@ def refreshScores() -> None: current_app.logger.debug('Attempting to load and parse scores file.') with open(SCORES_FILE, 'r') as file: temp = json.load(file) + if current_app.config['CONVERT_OLD']: + temp = convertFrom(temp) # Place all values into Team object for jinja temp = [ @@ -75,6 +77,10 @@ def refreshScores() -> None: def generateDemo() -> None: + """ + Generate a base demo scores file. Overwrites the given SCORES_FILE. + """ + fake = faker.Faker() data = [ { @@ -85,17 +91,22 @@ def generateDemo() -> None: ] with open(SCORES_FILE, 'w') as file: - json.dump(data, file) + json.dump(convertTo(data) if current_app.config['CONVERT_OLD'] else data, file) def alterDemo() -> None: + """ + Alters the current scores file. Intended for demo application mode. + Adds a new score each alteration. Triggers a application fresh with 'refreshScores' + """ + from trivia.create_app import scheduler app = scheduler.app with app.app_context(): current_app.logger.debug('Altering Demo Data...') with open(SCORES_FILE, 'r') as file: - data = json.load(file) + data = convertFrom(json.load(file)) if current_app.config['CONVERT_OLD'] else json.load(file) if len(data) > 0: if len(data[0]['scores']) >= current_app.config['DEMO_MAX_SCORES']: @@ -105,4 +116,41 @@ def alterDemo() -> None: team['scores'].append(random.randint(2, 8) if random.random() > 0.25 else 0) with open(SCORES_FILE, 'w') as file: - json.dump(data, file) + json.dump(convertTo(data) if current_app.config['CONVERT_OLD'] else data, file) + + refreshScores() + + +def convertFrom(data) -> List[dict]: + """ + Converts scores data from old to new format. + + :param data: Old format data + :return: Old format data + """ + return [ + { + 'teamno': oldteam['Team']['Number'], + 'teamname': oldteam['Team']['DisplayName'], + 'scores': oldteam['Scores'] + } + for oldteam in data + ] + + +def convertTo(data) -> List[dict]: + """ + Converst scores from new to old format + :param data: New format data + :return: Old format data + """ + return [ + { + 'Team': { + 'Number': team['teamno'], + 'DisplayName': team['teamname'] + }, + 'Scores': team['scores'], + 'TotalGuess': -1 + } for team in data + ]