mirror of
https://github.com/Xevion/trivia.git
synced 2025-12-07 03:16:48 -06:00
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:
@@ -1,2 +1,2 @@
|
||||
FLASK_APP=trivia.create_app
|
||||
FLASK_ENV=development
|
||||
FLASK_ENV=production
|
||||
@@ -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.
|
||||
|
||||
28
cli.py
28
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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -86,7 +86,11 @@
|
||||
{% for team in teams %}
|
||||
<tr id="{{ team.id }}">
|
||||
<td>{{ range(1, teams | length) | random }}</td>
|
||||
{% if team.name | length > 0 %}
|
||||
<td>{{ team.name }}</td>
|
||||
{% else %}
|
||||
<td>Team {{ team.id }}</td>
|
||||
{% endif %}
|
||||
{% for score in team.scores %}
|
||||
<td>{{ score }}</td>
|
||||
{% endfor %}
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user