mirror of
https://github.com/Xevion/trivia.git
synced 2025-12-08 06:08:44 -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_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
|
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
28
cli.py
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user