Implement backend for guess + result entering
Some checks failed
Build Formula10 Docker Image / build-docker (push) Failing after 11s

This commit is contained in:
2024-02-19 02:02:03 +01:00
parent 595b49c2b5
commit d0c7ba242c
2 changed files with 199 additions and 88 deletions

View File

@ -15,12 +15,13 @@ db.init_app(app)
# TODO # TODO
# General # General
# - Move guessed place to leftmost column and display actual finishing position of driver instead # - Store standing/dnf orders as dicts, since lists lose their order
# - Show coming race in table, to give better feedback once a user has locked in a guess # - Make user headers in race table clickable, to reach the specific page. Do the same for the season cards
# - Only allow guess entering in user-specific page # - When showing correct guesses in green, show semi-correct ones in a weaker tone (probably need to prepare those here, instead of in the template)
# - Remove whitespace from usernames # - Also show previous race results, and allow to change them. Or at least, allow editing the current one and show current state (do it like the activeuser select for results)
# - Remove whitespace from usernames + races. Sanitization should only happen inside the templates + endpoint controllers, for the URLs
# - Add doc comments to model
# - Sortable list to enter full race results (need 7 positions to calculate points) => remove from race page
# - Make the season card grid left-aligned? So e.g. 2 cards are not spread over the whole screen with large gaps? # - Make the season card grid left-aligned? So e.g. 2 cards are not spread over the whole screen with large gaps?
# - Choose "place to guess" late before the race? # - Choose "place to guess" late before the race?
# - Timer until season picks lock + next race timer # - Timer until season picks lock + next race timer
@ -28,7 +29,7 @@ db.init_app(app)
# Statistics page # Statistics page
# - Auto calculate points # - Auto calculate points
# - Generate static diagram using chart.js + templating the js (yikes) # - Generate static diagram using chart.js + templating the js (funny yikes)
# Rules page # Rules page
@ -75,40 +76,39 @@ def guessuserraceresults(username):
raceresults: List[RaceResult] = RaceResult.query.all()[::-1] raceresults: List[RaceResult] = RaceResult.query.all()[::-1]
drivers: List[Driver] = Driver.query.all() drivers: List[Driver] = Driver.query.all()
# Select User print(raceresults)
# chosenusers = users
# if username != "Everyone":
# chosenusers = [user for user in users if user.name == username]
guesses = dict() guesses: Dict[int, Dict[str, RaceGuess]] = dict()
guess: RaceGuess
for guess in RaceGuess.query.all(): for guess in RaceGuess.query.all():
if guess.race_id not in guesses: if guess.race_id not in guesses:
guesses[guess.race_id] = dict() guesses[guess.race_id] = dict()
guesses[guess.race_id][guess.user_id] = guess guesses[guess.race_id][guess.user_id] = guess
# nextid = raceresults[0].race_id + 1 if len(raceresults) > 0 else 1 nextid = raceresults[0].race_id + 1 if len(raceresults) > 0 else 1
# nextrace: Race = Race.query.filter_by(id=nextid).first() nextrace: Race | None = Race.query.filter_by(id=nextid).first()
return render_template("race.jinja", return render_template("race.jinja",
users=users, users=users,
drivers=drivers, drivers=drivers,
raceresults=raceresults, raceresults=raceresults,
guesses=guesses, guesses=guesses,
activeuser=activeuser) activeuser=activeuser,
currentrace=nextrace)
@app.route("/guessrace/<raceid>/<username>", methods=["POST"]) @app.route("/guessrace/<raceid>/<username>", methods=["POST"])
def guessrace(raceid, username): def guessrace(raceid, username):
pxx = request.form.get("pxxselect") pxx: str | None = request.form.get("pxxselect")
dnf = request.form.get("dnfselect") dnf: str | None = request.form.get("dnfselect")
if pxx is None or dnf is None: if pxx is None or dnf is None:
return redirect("/race") return redirect(f"/race/{username}")
if RaceResult.query.filter_by(race_id=raceid).first() is not None: if RaceResult.query.filter_by(race_id=raceid).first() is not None:
print("Error: Can't guess race result if the race result is already known!") print("Error: Can't guess race result if the race result is already known!")
return redirect("/race") return redirect(f"/race/{username}")
raceguess: RaceGuess | None = RaceGuess.query.filter_by(user_id=username, race_id=raceid).first() raceguess: RaceGuess | None = RaceGuess.query.filter_by(user_id=username, race_id=raceid).first()
@ -125,30 +125,6 @@ def guessrace(raceid, username):
return redirect("/race") return redirect("/race")
# @app.route("/enterresult/<raceid>", methods=["POST"])
# def enterresult(raceid):
# pxx = request.form.get("pxxselect")
# dnf = request.form.get("dnfselect")
#
# if pxx is None or dnf is None:
# return redirect("/race")
#
# raceresult: RaceResult | None = RaceResult.query.filter_by(race_id=raceid).first()
#
# if raceresult is not None:
# print("RaceResult already exists!")
# return redirect("/race")
#
# raceresult = RaceResult()
# raceresult.race_id = raceid
# raceresult.pxx_id = pxx
# raceresult.dnf_id = dnf
# db.session.add(raceresult)
# db.session.commit()
#
# return redirect("/race")
@app.route("/season") @app.route("/season")
def guessseasonresults(): def guessseasonresults():
return redirect("/season/Everyone") return redirect("/season/Everyone")
@ -164,12 +140,12 @@ def guessuserseasonresults(username):
# Remove NONE driver # Remove NONE driver
drivers = [driver for driver in drivers if driver.name != "NONE"] drivers = [driver for driver in drivers if driver.name != "NONE"]
guesses = dict() guesses: Dict[str, SeasonGuess] = dict()
guess: SeasonGuess guess: SeasonGuess
for guess in SeasonGuess.query.all(): for guess in SeasonGuess.query.all():
guesses[guess.user_id] = guess guesses[guess.user_id] = guess
driverpairs = dict() driverpairs: Dict[str, List[Driver]] = dict()
team: Team team: Team
for team in teams: for team in teams:
driverpairs[team.name] = [] driverpairs[team.name] = []
@ -187,8 +163,8 @@ def guessuserseasonresults(username):
@app.route("/guessseason/<username>", methods=["POST"]) @app.route("/guessseason/<username>", methods=["POST"])
def guessseason(username): def guessseason(username: str):
guesses = [ guesses: List[str | None] = [
request.form.get("hottakeselect"), request.form.get("hottakeselect"),
request.form.get("p2select"), request.form.get("p2select"),
request.form.get("overtakeselect"), request.form.get("overtakeselect"),
@ -196,10 +172,10 @@ def guessseason(username):
request.form.get("gainedselect"), request.form.get("gainedselect"),
request.form.get("lostselect") request.form.get("lostselect")
] ]
teamwinnerguesses = [ teamwinnerguesses: List[str | None] = [
request.form.get(f"teamwinner-{team.name}") for team in Team.query.all() request.form.get(f"teamwinner-{team.name}") for team in Team.query.all()
] ]
podiumdriverguesses = request.form.getlist("podiumdrivers") podiumdriverguesses: List[str] = request.form.getlist("podiumdrivers")
if any(guess is None for guess in guesses + teamwinnerguesses): if any(guess is None for guess in guesses + teamwinnerguesses):
print("Error: /guessseason could not obtain request data!") print("Error: /guessseason could not obtain request data!")
@ -213,7 +189,7 @@ def guessseason(username):
teamwinners = TeamWinners() teamwinners = TeamWinners()
db.session.add(teamwinners) db.session.add(teamwinners)
teamwinners.winner_ids = teamwinnerguesses teamwinners.winner_ids = teamwinnerguesses # Pylance throws error, but nullcheck is done
teamwinners.user_id = username teamwinners.user_id = username
db.session.commit() db.session.commit()
@ -234,27 +210,86 @@ def guessseason(username):
seasonguess.user_id = username seasonguess.user_id = username
db.session.add(seasonguess) db.session.add(seasonguess)
seasonguess.hot_take = guesses[0] seasonguess.hot_take = guesses[0] # Pylance throws error but nullcheck is done
seasonguess.p2_constructor_id = guesses[1] seasonguess.p2_constructor_id = guesses[1] # Pylance throws error but nullcheck is done
seasonguess.most_overtakes_id = guesses[2] seasonguess.most_overtakes_id = guesses[2] # Pylance throws error but nullcheck is done
seasonguess.most_dnfs_id = guesses[3] seasonguess.most_dnfs_id = guesses[3] # Pylance throws error but nullcheck is done
seasonguess.most_gained_id = guesses[4] seasonguess.most_gained_id = guesses[4] # Pylance throws error but nullcheck is done
seasonguess.most_lost_id = guesses[5] seasonguess.most_lost_id = guesses[5] # Pylance throws error but nullcheck is done
seasonguess.team_winners_id = teamwinners.id seasonguess.team_winners_id = teamwinners.id # Pylance throws error but nullcheck is done
seasonguess.podium_drivers_id = podiumdrivers.id seasonguess.podium_drivers_id = podiumdrivers.id # Pylance throws error but nullcheck is done
db.session.commit() db.session.commit()
return redirect("/season") return redirect("/season")
@app.route("/enter") @app.route("/enter")
def enterraceresult(): def entercurrentraceresult():
return render_template("enter.jinja") return redirect("/enter/Current")
@app.route("/enter/<resultname>")
def enterraceresult(resultname: str):
drivers: List[Driver] = Driver.query.all()
raceresults: List[RaceResult] = RaceResult.query.all()[::-1]
# Find next race without result
nextid = raceresults[0].race_id + 1 if len(raceresults) > 0 else 1
nextrace: Race | None = Race.query.filter_by(id=nextid).first()
activeresult: RaceResult | None = None
if resultname != "Current":
# Obtain the chosen result
activeresult = list(filter(lambda result: result.race.grandprix == resultname, raceresults))[0]
else:
# Obtain the current result if it exists
activeresult = raceresults[0] if len(raceresults) > 0 and raceresults[0].race_id == nextrace else None
# Remove NONE driver
drivers = [driver for driver in drivers if driver.name != "NONE"]
return render_template("enter.jinja",
drivers=drivers,
race=nextrace,
results=raceresults,
activeresult=activeresult)
@app.route("/enterresult/<raceid>", methods=["POST"])
def enterresult(raceid):
pxxs: List[str] = request.form.getlist("pxxdrivers")
dnfs: List[str] = request.form.getlist("dnf-drivers")
excludes: List[str] = request.form.getlist("exclude-drivers")
# Use strings as keys, as these dicts will be serialized to json
pxxs_dict: Dict[str, str] = {str(position + 1): driver for position, driver in enumerate(pxxs)}
dnfs_dict: Dict[str, str] = {str(position + 1): driver for position, driver in enumerate(pxxs) if driver in dnfs}
print(pxxs_dict)
raceresult: RaceResult | None = RaceResult.query.filter_by(race_id=raceid).first()
if raceresult is None:
raceresult = RaceResult()
db.session.add(raceresult)
raceresult.race_id = raceid
raceresult.pxx_ids = pxxs_dict
raceresult.dnf_ids = dnfs_dict if len(dnfs) > 0 else {"20": "NONE"}
raceresult.exclude_ids = excludes
db.session.commit()
race: Race | None = Race.query.filter_by(id=raceid).first()
if race is None:
print("Error: Can't redirect to /enter/<GrandPrix> because race couldn't be found")
return redirect("/enter")
return redirect(f"/enter/{race.grandprix}")
@app.route("/users") @app.route("/users")
def manageusers(): def manageusers():
users = User.query.all() users: List[User] = User.query.all()
return render_template("users.jinja", return render_template("users.jinja",
users=users) users=users)
@ -262,7 +297,11 @@ def manageusers():
@app.route("/adduser", methods=["POST"]) @app.route("/adduser", methods=["POST"])
def adduser(): def adduser():
username = request.form.get("select-add-user").strip() username: str | None = request.form.get("select-add-user")
if username is None or len(username) == 0:
print(f"Not adding user, since no username was received")
return redirect("/users")
if len(User.query.filter_by(name=username).all()) > 0: if len(User.query.filter_by(name=username).all()) > 0:
print(f"Not adding user {username}: Already exists!") print(f"Not adding user {username}: Already exists!")
@ -280,6 +319,10 @@ def adduser():
def deleteuser(): def deleteuser():
username = request.form.get("select-delete-user") username = request.form.get("select-delete-user")
if username is None or len(username) == 0:
print(f"Not deleting user, since no username was received")
return redirect("/users")
if username == "Select User": if username == "Select User":
return redirect("/users") return redirect("/users")

122
model.py
View File

@ -1,4 +1,4 @@
from typing import List from typing import ClassVar, List, Dict
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, PickleType from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, PickleType
@ -14,6 +14,10 @@ db = SQLAlchemy()
class Race(db.Model): class Race(db.Model):
"""
A single race at a certain date and GrandPrix in the calendar.
It stores the place to guess for this race.
"""
__tablename__ = "race" __tablename__ = "race"
def from_csv(self, row): def from_csv(self, row):
@ -32,6 +36,9 @@ class Race(db.Model):
class Team(db.Model): class Team(db.Model):
"""
A constructor/team (name only).
"""
__tablename__ = "team" __tablename__ = "team"
def from_csv(self, row): def from_csv(self, row):
@ -42,6 +49,10 @@ class Team(db.Model):
class Driver(db.Model): class Driver(db.Model):
"""
A F1 driver.
It stores the corresponding team + name abbreviation.
"""
__tablename__ = "driver" __tablename__ = "driver"
def from_csv(self, row): def from_csv(self, row):
@ -66,6 +77,9 @@ class Driver(db.Model):
class User(db.Model): class User(db.Model):
"""
A user that can guess races (name only).
"""
__tablename__ = "user" __tablename__ = "user"
__csv_header__ = ["name"] __csv_header__ = ["name"]
@ -82,7 +96,12 @@ class User(db.Model):
class RaceResult(db.Model): class RaceResult(db.Model):
"""
The result of a past race.
It stores the corresponding race and dictionaries of place-/dnf-order and a list of drivers that are excluded from the standings for this race.
"""
__tablename__ = "raceresult" __tablename__ = "raceresult"
__allow_unmapped__ = True
__csv_header__ = ["id", "race_id", "pxx_ids_json", "dnf_ids_json", "exclude_ids_json"] __csv_header__ = ["id", "race_id", "pxx_ids_json", "dnf_ids_json", "exclude_ids_json"]
def from_csv(self, row): def from_csv(self, row):
@ -109,19 +128,19 @@ class RaceResult(db.Model):
exclude_ids_json: Mapped[str] = mapped_column(String(1024)) exclude_ids_json: Mapped[str] = mapped_column(String(1024))
@property @property
def pxx_ids(self) -> List[str]: def pxx_ids(self) -> Dict[str, str]:
return json.loads(self.pxx_ids_json) return json.loads(self.pxx_ids_json)
@pxx_ids.setter @pxx_ids.setter
def pxx_ids(self, new_pxx_ids: List[str]): def pxx_ids(self, new_pxx_ids: Dict[str, str]):
self.pxx_ids_json = json.dumps(new_pxx_ids) self.pxx_ids_json = json.dumps(new_pxx_ids)
@property @property
def dnf_ids(self) -> List[str]: def dnf_ids(self) -> Dict[str, str]:
return json.loads(self.dnf_ids_json) return json.loads(self.dnf_ids_json)
@dnf_ids.setter @dnf_ids.setter
def dnf_ids(self, new_dnf_ids: List[str]): def dnf_ids(self, new_dnf_ids: Dict[str, str]):
self.dnf_ids_json = json.dumps(new_dnf_ids) self.dnf_ids_json = json.dumps(new_dnf_ids)
@property @property
@ -134,39 +153,68 @@ class RaceResult(db.Model):
# Relationships # Relationships
race: Mapped["Race"] = relationship("Race", foreign_keys=[race_id]) race: Mapped["Race"] = relationship("Race", foreign_keys=[race_id])
_pxxs = None _pxxs: Dict[str, Driver] | None = None
_dnfs = None _dnfs: Dict[str, Driver] | None = None
_excludes = None _excludes: List[Driver] | None = None
@property @property
def pxxs(self) -> List[Driver]: def pxxs(self) -> Dict[str, Driver]:
if self._pxxs is None: if self._pxxs is None:
self._pxxs = [ self._pxxs = dict()
driver for driver in Driver.query.all() if driver.name in self.pxx_ids for position, driver_id in self.pxx_ids.items():
] driver = Driver.query.filter_by(name=driver_id).first()
if driver is None:
raise Exception(f"Error: Couldn't find driver with id {driver_id}")
self._pxxs[position] = driver
return self._pxxs return self._pxxs
@property @property
def dnfs(self) -> List[Driver]: def dnfs(self) -> Dict[str, Driver]:
if self._dnfs is None: if self._dnfs is None:
self._dnfs = [ self._dnfs = dict()
driver for driver in Driver.query.all() if driver.name in self.dnf_ids for position, driver_id in self.dnf_ids.items():
] driver = Driver.query.filter_by(name=driver_id).first()
if driver is None:
raise Exception(f"Error: Couldn't find driver with id {driver_id}")
self._dnfs[position] = driver
return self._dnfs return self._dnfs
@property @property
def excludes(self) -> List[Driver]: def excludes(self) -> List[Driver]:
if self._excludes is None: if self._excludes is None:
self._excludes = [ self._excludes = []
driver for driver in Driver.query.all() if driver.name in self.exclude_ids for driver_id in self.exclude_ids:
] driver = Driver.query.filter_by(name=driver_id).first()
if driver is None:
raise Exception(f"Error: Couldn't find driver with id {driver_id}")
self._excludes += [driver]
return self._excludes return self._excludes
@property
def pxx(self) -> Driver:
pxx_num: str = str(self.race.pxx)
if pxx_num not in self.pxxs:
print(self.pxxs)
raise Exception(f"Error: Position {self.race.pxx} not contained in race result")
return self.pxxs[pxx_num]
@property
def dnf(self) -> Driver:
return sorted(self.dnfs.items(), reverse=True)[0][1]
class RaceGuess(db.Model): class RaceGuess(db.Model):
"""
A guess a user made for a race.
It stores the corresponding race and the guessed drivers for PXX and DNF.
"""
__tablename__ = "raceguess" __tablename__ = "raceguess"
__csv_header__ = ["id", "user_id", "race_id", "pxx_id", "dnf_id"] __csv_header__ = ["id", "user_id", "race_id", "pxx_id", "dnf_id"]
@ -201,7 +249,11 @@ class RaceGuess(db.Model):
class TeamWinners(db.Model): class TeamWinners(db.Model):
"""
A guessed list of each best driver per team.
"""
__tablename__ = "teamwinners" __tablename__ = "teamwinners"
__allow_unmapped__ = True
__csv_header__ = ["id", "user_id", "winner_ids_json"] __csv_header__ = ["id", "user_id", "winner_ids_json"]
def from_csv(self, row): def from_csv(self, row):
@ -231,20 +283,29 @@ class TeamWinners(db.Model):
# Relationships # Relationships
user: Mapped["User"] = relationship("User", foreign_keys=[user_id]) user: Mapped["User"] = relationship("User", foreign_keys=[user_id])
_winners = None _winners: List[Driver] | None = None
@property @property
def winners(self) -> List[Driver]: def winners(self) -> List[Driver]:
if self._winners is None: if self._winners is None:
self._winners = [ self._winners = []
driver for driver in Driver.query.all() if driver.name in self.winner_ids for driver_id in self.winner_ids:
] driver = Driver.query.filter_by(name=driver_id).first()
if driver is None:
raise Exception(f"Error: Couldn't find driver with id {driver_id}")
self._winners += [driver]
return self._winners return self._winners
class PodiumDrivers(db.Model): class PodiumDrivers(db.Model):
"""
A guessed list of each driver that will reach at least a single podium.
"""
__tablename__ = "podiumdrivers" __tablename__ = "podiumdrivers"
__allow_unmapped__ = True
__csv_header__ = ["id", "user_id", "podium_ids_json"] __csv_header__ = ["id", "user_id", "podium_ids_json"]
def from_csv(self, row): def from_csv(self, row):
@ -274,19 +335,26 @@ class PodiumDrivers(db.Model):
# Relationships # Relationships
user: Mapped["User"] = relationship("User", foreign_keys=[user_id]) user: Mapped["User"] = relationship("User", foreign_keys=[user_id])
_podiums = None _podiums: List[Driver] | None = None
@property @property
def podiums(self) -> List[Driver]: def podiums(self) -> List[Driver]:
if self._podiums is None: if self._podiums is None:
self._podiums = [ self._podiums = []
driver for driver in Driver.query.all() if driver.name in self.podium_ids for driver_id in self.podium_ids:
] driver = Driver.query.filter_by(name=driver_id).first()
if driver is None:
raise Exception(f"Error: Couldn't find driver with id {driver_id}")
self._podiums += [driver]
return self._podiums return self._podiums
class SeasonGuess(db.Model): class SeasonGuess(db.Model):
"""
A collection of bonus guesses for the entire season.
"""
__tablename__ = "seasonguess" __tablename__ = "seasonguess"
__csv_header__ = ["id", "user_id", __csv_header__ = ["id", "user_id",
"hot_take", "p2_constructor_id", "most_overtakes_id", "most_dnfs_id", "most_gained_id", "hot_take", "p2_constructor_id", "most_overtakes_id", "most_dnfs_id", "most_gained_id",