added proper DEBUG prints, attempts at fixing timezone fails with If-Modified-Since header, conversion methods between old and new scores data format

This commit is contained in:
Xevion
2020-06-22 16:10:49 -05:00
parent 952f1c22a2
commit bddc0b9746
8 changed files with 106 additions and 30 deletions

View File

@@ -1,2 +1,2 @@
FLASK_APP=trivia.create_app FLASK_APP=trivia.create_app
FLASK_ENV=development FLASK_ENV=production

View File

@@ -11,9 +11,11 @@ on at `localhost:5000` unless
git clone https://github.com/Xevion/trivia.git git clone https://github.com/Xevion/trivia.git
cd trivia cd trivia
pip install -r requirements.txt pip install -r requirements.txt
python wsgi.py flask run
``` ```
Edit .flaskenv to change the configuration preset.
## Application Design ## Application Design
The webapp has two sides: Client and Server. The webapp has two sides: Client and Server.

28
cli.py
View File

@@ -5,7 +5,6 @@ A simple CLI implementation using the application's API.
""" """
import curses import curses
import time
from datetime import datetime from datetime import datetime
from typing import List from typing import List
@@ -18,7 +17,7 @@ lastAttempt: float = -1
lastUpdate: float = -1 lastUpdate: float = -1
def refreshScores() -> bool: def refreshScores() -> None:
""" """
Refreshes scoreboard data safely, handling a unresponsive or downed scoreboard. Refreshes scoreboard data safely, handling a unresponsive or downed scoreboard.
Uses If-Modified-Since headers properly. Uses If-Modified-Since headers properly.
@@ -30,21 +29,24 @@ def refreshScores() -> bool:
global lastUpdate, lastAttempt, scores global lastUpdate, lastAttempt, scores
# Send with If-Modified-Since header if this is not the first time # 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 # Send request with headers
try: try:
resp = requests.get('http://127.0.0.1:5000/api/scores/', headers=headers) resp = requests.get('http://127.0.0.1:5000/api/scores/', headers=headers)
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
resp = None resp = None
finally: finally:
lastAttempt = time.time() lastAttempt = datetime.utcnow().timestamp()
if resp is not None and resp.ok: if resp is not None and resp.ok:
if resp.status_code == 304 and len(scores) != 0: if resp.status_code == 304 and len(scores) != 0:
pass pass
else: else:
# Changes found, update! # Changes found, update!
lastUpdate = time.time() lastUpdate = datetime.utcnow().timestamp()
scores = resp.json() scores = resp.json()
# Calculate totals, preliminary sort by total # Calculate totals, preliminary sort by total
@@ -54,25 +56,25 @@ def refreshScores() -> bool:
# Calculate ranks with tie handling logic # Calculate ranks with tie handling logic
for i, team in enumerate(scores): 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']: if i > 0 and scores[i - 1]['total'] == team['total']:
team['rank'] = scores[i - 1]['rank'] team['rank'] = scores[i - 1]['rank']
# Check if we have a T
if not team['rank'].startswith('T'): if not team['rank'].startswith('T'):
team['rank'] = 'T' + team['rank'].strip() team['rank'] = 'T' + team['rank'].strip()
# Check if previous score has a T
if not scores[i - 1]['rank'].startswith('T'): if not scores[i - 1]['rank'].startswith('T'):
scores[i - 1]['rank'] = 'T' + scores[i - 1]['rank'].strip() scores[i - 1]['rank'] = 'T' + scores[i - 1]['rank'].strip()
else: else:
# Otherwise just add a space in front instead
team['rank'] = " " + str(i + 1) team['rank'] = " " + str(i + 1)
return True
return False
def main(screen) -> None: def main(screen) -> None:
""" """
Mainloop function Mainloop function.
:param screen: Curses screen :param screen: Curses screen
""" """
@@ -80,7 +82,7 @@ def main(screen) -> None:
screen.redrawwin() screen.redrawwin()
while True: while True:
# Refresh scores every 10 seconds # Refresh scores every 10 seconds
if time.time() - lastAttempt > 0.5: if datetime.utcnow().timestamp() - lastAttempt > 0.5:
refreshScores() refreshScores()
# Get current terminal size and clear # Get current terminal size and clear
@@ -90,6 +92,7 @@ def main(screen) -> None:
# Build table data # Build table data
global scores global scores
table = [[team['rank'], team['id'], team['name'], team['total'], *team['scores']] for team in scores[:y - 4]] 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 [] 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.insert(0, ['Rank', 'ID', 'Team Name', 'T', *scoreSet])
table = SingleTable(table, title='EfTA Trivia Night') table = SingleTable(table, title='EfTA Trivia Night')
@@ -104,7 +107,8 @@ def main(screen) -> None:
# Terminal Size # Terminal Size
strpos = str((x, y)) strpos = str((x, y))
screen.addstr(y - 1, 1, strpos) 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 # Update curses screen
screen.refresh() screen.refresh()

View File

@@ -27,7 +27,14 @@ def scores():
try: try:
if request.headers['If-Modified-Since']: if request.headers['If-Modified-Since']:
# Acquire epoch time from header # 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: if epoch >= lastChange:
status_code = 304 status_code = 304
except KeyError: except KeyError:

View File

@@ -6,6 +6,7 @@ config.py
configs = { configs = {
'production': 'trivia.config.Config', 'production': 'trivia.config.Config',
'productiondep': 'trivia.config.ConfigDeprecated',
'development': 'trivia.config.Config', 'development': 'trivia.config.Config',
'demo': 'trivia.config.DemoConfig' 'demo': 'trivia.config.DemoConfig'
} }
@@ -15,6 +16,8 @@ class Config(object):
# Main Configuration # Main Configuration
SCORE_FILE = 'scores.json' SCORE_FILE = 'scores.json'
POLLING_INTERVAL = 5 POLLING_INTERVAL = 5
DEBUG = False
CONVERT_OLD = True
# Demo Configuration # Demo Configuration
DEMO = False DEMO = False
@@ -23,9 +26,14 @@ class Config(object):
DEMO_MAX_SCORES = 0 DEMO_MAX_SCORES = 0
def ConfigDeprecated(Config):
CONVERT_OLD = True
class DemoConfig(Config): class DemoConfig(Config):
# Main Configuration # Main Configuration
SCORE_FILE = 'demo.json' SCORE_FILE = 'demo.json'
DEBUG = True
# Demo Configuration # Demo Configuration
DEMO = True DEMO = True

View File

@@ -1,10 +1,11 @@
from flask_apscheduler import APScheduler
from flask import Flask from flask import Flask
from flask_apscheduler import APScheduler
from trivia.config import configs from trivia.config import configs
scheduler: APScheduler = None scheduler: APScheduler = None
def create_app(env=None): def create_app(env=None):
app = Flask(__name__) app = Flask(__name__)
@@ -23,14 +24,16 @@ def create_app(env=None):
scheduler.start() scheduler.start()
# Add score file polling # 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']: if app.config['DEMO']:
app.logger.info('Generating Demo Data...') app.logger.info('Generating Demo Data...')
# Generate initial Demo data # Generate initial Demo data
utils.generateDemo() utils.generateDemo()
# Begin altering demo data regularly # 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() utils.refreshScores()

View File

@@ -86,7 +86,11 @@
{% for team in teams %} {% for team in teams %}
<tr id="{{ team.id }}"> <tr id="{{ team.id }}">
<td>{{ range(1, teams | length) | random }}</td> <td>{{ range(1, teams | length) | random }}</td>
{% if team.name | length > 0 %}
<td>{{ team.name }}</td> <td>{{ team.name }}</td>
{% else %}
<td>Team {{ team.id }}</td>
{% endif %}
{% for score in team.scores %} {% for score in team.scores %}
<td>{{ score }}</td> <td>{{ score }}</td>
{% endfor %} {% endfor %}

View File

@@ -5,9 +5,10 @@ Stores important backend application functionality.
""" """
import json import json
import os import os
import time
import random import random
import time
from collections import namedtuple from collections import namedtuple
from datetime import datetime
from typing import List from typing import List
# Simple fake 'class' for passing to jinja templates # 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. 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: def refreshScores() -> None:
""" """
Refreshes scores data safely. Refreshes scores data safely.
:return:
""" """
global lastChange
curChange = lastModified()
from trivia.create_app import scheduler from trivia.create_app import scheduler
app = scheduler.app app = scheduler.app
with app.app_context(): with app.app_context():
global lastChange
curChange = lastModified()
if lastChange < curChange: if lastChange < curChange:
try: try:
# Update tracking var # Update tracking var
@@ -55,6 +55,8 @@ def refreshScores() -> None:
current_app.logger.debug('Attempting to load and parse scores file.') current_app.logger.debug('Attempting to load and parse scores file.')
with open(SCORES_FILE, 'r') as file: with open(SCORES_FILE, 'r') as file:
temp = json.load(file) temp = json.load(file)
if current_app.config['CONVERT_OLD']:
temp = convertFrom(temp)
# Place all values into Team object for jinja # Place all values into Team object for jinja
temp = [ temp = [
@@ -75,6 +77,10 @@ def refreshScores() -> None:
def generateDemo() -> None: def generateDemo() -> None:
"""
Generate a base demo scores file. Overwrites the given SCORES_FILE.
"""
fake = faker.Faker() fake = faker.Faker()
data = [ data = [
{ {
@@ -85,17 +91,22 @@ def generateDemo() -> None:
] ]
with open(SCORES_FILE, 'w') as file: 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: 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 from trivia.create_app import scheduler
app = scheduler.app app = scheduler.app
with app.app_context(): with app.app_context():
current_app.logger.debug('Altering Demo Data...') current_app.logger.debug('Altering Demo Data...')
with open(SCORES_FILE, 'r') as file: 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:
if len(data[0]['scores']) >= current_app.config['DEMO_MAX_SCORES']: 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) team['scores'].append(random.randint(2, 8) if random.random() > 0.25 else 0)
with open(SCORES_FILE, 'w') as file: 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
]