diff --git a/database_utils.py b/database_utils.py index c7942fd..1fba9ff 100644 --- a/database_utils.py +++ b/database_utils.py @@ -51,7 +51,7 @@ def reload_static_data(db): def reload_dynamic_data(db): print("Initializing Database with Dynamic Values...") - + # Create it (if it doesn't exist!) db.create_all() # Clear dynamic data diff --git a/formula10.py b/formula10.py index 3e6c388..966c35a 100644 --- a/formula10.py +++ b/formula10.py @@ -1,8 +1,7 @@ -from typing import List - from flask import Flask, render_template, request, redirect from model import * from database_utils import reload_static_data, reload_dynamic_data, export_dynamic_data +from template_model import TemplateModel app = Flask(__name__) @@ -14,11 +13,10 @@ db.init_app(app) # TODO # General -# - Make user headers in race table clickable, to reach the specific page. Do the same for the season cards -# - 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) -# - 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 + +# - 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) +# - Show cards of previous race results, like with season guesses? # - 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? @@ -33,135 +31,90 @@ db.init_app(app) @app.route("/") -def index(): - return redirect("/race") +def root(): + return race_active_user("Everyone") -@app.route("/saveall") -def savedynamic(): +@app.route("/save/all", strict_slashes=False) +def save(): export_dynamic_data() return redirect("/") -@app.route("/loadall") +@app.route("/load/all") def load(): reload_static_data(db) reload_dynamic_data(db) return redirect("/") -@app.route("/loadstatic") -def reloadstatic(): +@app.route("/load/static") +def load_static(): reload_static_data(db) return redirect("/") -@app.route("/loaddynamic") -def reloaddynamic(): +@app.route("/load/dynamic") +def load_dynamic(): reload_dynamic_data(db) return redirect("/") @app.route("/race") -def guessraceresult(): +def race_root(): return redirect("/race/Everyone") -@app.route("/race/") -def guessuserraceresults(username): - users: List[User] = User.query.all() - activeuser: User | None = User.query.filter_by(name=username).first() # "Everyone" should yield None - raceresults: List[RaceResult] = RaceResult.query.all()[::-1] - drivers: List[Driver] = Driver.query.all() - - print(raceresults) - - guesses: Dict[int, Dict[str, RaceGuess]] = dict() - guess: RaceGuess - for guess in RaceGuess.query.all(): - if guess.race_id not in guesses: - guesses[guess.race_id] = dict() - - guesses[guess.race_id][guess.user_id] = guess - - nextid = raceresults[0].race_id + 1 if len(raceresults) > 0 else 1 - nextrace: Race | None = Race.query.filter_by(id=nextid).first() - +@app.route("/race/") +def race_active_user(user_name: str): + model = TemplateModel() return render_template("race.jinja", - users=users, - drivers=drivers, - raceresults=raceresults, - guesses=guesses, - activeuser=activeuser, - currentrace=nextrace) + active_user=model.user_by(user_name=user_name, ignore=["Everyone"]), + model=model) -@app.route("/guessrace//", methods=["POST"]) -def guessrace(raceid, username): +@app.route("/race-guess//", methods=["POST"]) +def race_guess_post(race_name: str, user_name: str): pxx: str | None = request.form.get("pxxselect") dnf: str | None = request.form.get("dnfselect") if pxx is None or dnf is None: - return redirect(f"/race/{username}") + return race_active_user(user_name) - if RaceResult.query.filter_by(race_id=raceid).first() is not None: + if RaceResult.query.filter_by(race_name=race_name).first() is not None: print("Error: Can't guess race result if the race result is already known!") - return redirect(f"/race/{username}") + return redirect(f"/race/{user_name}") - raceguess: RaceGuess | None = RaceGuess.query.filter_by(user_id=username, race_id=raceid).first() + raceguess: RaceGuess | None = RaceGuess.query.filter_by(user_name=user_name, race_name=race_name).first() if raceguess is None: raceguess = RaceGuess() - raceguess.user_id = username - raceguess.race_id = raceid + raceguess.user_name = user_name + raceguess.race_name = race_name db.session.add(raceguess) - raceguess.pxx_id = pxx - raceguess.dnf_id = dnf + raceguess.pxx_driver_name = pxx + raceguess.dnf_driver_name = dnf db.session.commit() - return redirect("/race") + return redirect("/race/Everyone") @app.route("/season") -def guessseasonresults(): +def season_root(): return redirect("/season/Everyone") -@app.route("/season/") -def guessuserseasonresults(username): - users: List[User] = User.query.all() - activeuser: User | None = User.query.filter_by(name=username).first() - teams: List[Team] = Team.query.all() - drivers: List[Driver] = Driver.query.all() - - # Remove NONE driver - drivers = [driver for driver in drivers if driver.name != "NONE"] - - guesses: Dict[str, SeasonGuess] = dict() - guess: SeasonGuess - for guess in SeasonGuess.query.all(): - guesses[guess.user_id] = guess - - driverpairs: Dict[str, List[Driver]] = dict() - team: Team - for team in teams: - driverpairs[team.name] = [] - driver: Driver - for driver in drivers: - driverpairs[driver.team.name] += [driver] - +@app.route("/season/") +def season_active_user(user_name: str): + model = TemplateModel() return render_template("season.jinja", - users=users, - teams=teams, - drivers=drivers, - driverpairs=driverpairs, - guesses=guesses, - activeuser=activeuser) + active_user=model.user_by(user_name=user_name, ignore=["Everyone"]), + model=model) -@app.route("/guessseason/", methods=["POST"]) -def guessseason(username: str): +@app.route("/season-guess/", methods=["POST"]) +def season_guess_post(user_name: str): guesses: List[str | None] = [ request.form.get("hottakeselect"), request.form.get("p2select"), @@ -177,9 +130,9 @@ def guessseason(username: str): if any(guess is None for guess in guesses + teamwinnerguesses): print("Error: /guessseason could not obtain request data!") - return redirect("/season") + return redirect(f"/season/{user_name}") - seasonguess: SeasonGuess | None = SeasonGuess.query.filter_by(user_id=username).first() + seasonguess: SeasonGuess | None = SeasonGuess.query.filter_by(user_name=user_name).first() teamwinners: TeamWinners | None = seasonguess.team_winners if seasonguess is not None else None podiumdrivers: PodiumDrivers | None = seasonguess.podium_drivers if seasonguess is not None else None @@ -187,74 +140,49 @@ def guessseason(username: str): teamwinners = TeamWinners() db.session.add(teamwinners) - teamwinners.winner_ids = teamwinnerguesses # Pylance throws error, but nullcheck is done - teamwinners.user_id = username + teamwinners.user_name = user_name + teamwinners.teamwinner_driver_names = teamwinnerguesses # Pylance throws error, but nullcheck is done db.session.commit() if podiumdrivers is None: podiumdrivers = PodiumDrivers() db.session.add(podiumdrivers) - podiumdrivers.podium_ids = podiumdriverguesses - podiumdrivers.user_id = username + podiumdrivers.podium_driver_names = podiumdriverguesses + podiumdrivers.user_name = user_name db.session.commit() - # Refresh teamwinners + podiumdrivers to obtain IDs - teamwinners = TeamWinners.query.filter_by(user_id=username).first() - podiumdrivers = PodiumDrivers.query.filter_by(user_id=username).first() - if seasonguess is None: seasonguess = SeasonGuess() - seasonguess.user_id = username + seasonguess.user_name = user_name db.session.add(seasonguess) seasonguess.hot_take = guesses[0] # Pylance throws error but nullcheck is done - seasonguess.p2_constructor_id = guesses[1] # Pylance throws error but nullcheck is done - seasonguess.most_overtakes_id = guesses[2] # Pylance throws error but nullcheck is done - seasonguess.most_dnfs_id = guesses[3] # Pylance throws error but nullcheck is done - seasonguess.most_gained_id = guesses[4] # Pylance throws error but nullcheck is done - seasonguess.most_lost_id = guesses[5] # Pylance throws error but nullcheck is done - seasonguess.team_winners_id = teamwinners.id # Pylance throws error but nullcheck is done - seasonguess.podium_drivers_id = podiumdrivers.id # Pylance throws error but nullcheck is done + seasonguess.p2_team_name = guesses[1] # Pylance throws error but nullcheck is done + seasonguess.overtake_driver_name = guesses[2] # Pylance throws error but nullcheck is done + seasonguess.dnf_driver_name = guesses[3] # Pylance throws error but nullcheck is done + seasonguess.gained_driver_name = guesses[4] # Pylance throws error but nullcheck is done + seasonguess.lost_driver_name = guesses[5] # Pylance throws error but nullcheck is done db.session.commit() - return redirect("/season") + return redirect(f"/season/{user_name}") -@app.route("/enter") -def entercurrentraceresult(): - return redirect("/enter/Current") +@app.route("/result") +def result_root(): + return redirect("/result/Current") -@app.route("/enter/") -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"] - +@app.route("/result/") +def result_active_race(race_name: str): + model = TemplateModel() return render_template("enter.jinja", - drivers=drivers, - race=nextrace, - results=raceresults, - activeresult=activeresult) + active_result=model.race_result_by(race_name=race_name), + model=model) -@app.route("/enterresult/", methods=["POST"]) -def enterresult(raceid): +@app.route("/result-enter/", methods=["POST"]) +def result_enter_post(result_race_name: str): pxxs: List[str] = request.form.getlist("pxxdrivers") dnfs: List[str] = request.form.getlist("dnf-drivers") excludes: List[str] = request.form.getlist("exclude-drivers") @@ -263,74 +191,73 @@ def enterresult(raceid): 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() + raceresult: RaceResult | None = RaceResult.query.filter_by(race_name=result_race_name).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 + raceresult.race_name = result_race_name + raceresult.pxx_driver_names = pxxs_dict + raceresult.dnf_driver_names = dnfs_dict if len(dnfs) > 0 else {"20": "NONE"} + raceresult.excluded_driver_names = excludes db.session.commit() - race: Race | None = Race.query.filter_by(id=raceid).first() + race: Race | None = Race.query.filter_by(name=result_race_name).first() if race is None: print("Error: Can't redirect to /enter/ because race couldn't be found") - return redirect("/enter") + return redirect(f"/result/Current") - return redirect(f"/enter/{race.grandprix}") + return redirect(f"/result/{race.name}") -@app.route("/users") -def manageusers(): +@app.route("/user") +def user_root(): users: List[User] = User.query.all() return render_template("users.jinja", users=users) -@app.route("/adduser", methods=["POST"]) -def adduser(): +@app.route("/user-add", methods=["POST"]) +def user_add_post(): 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") + return user_root() if len(User.query.filter_by(name=username).all()) > 0: print(f"Not adding user {username}: Already exists!") - return redirect("/users") + return redirect("/user") user = User() user.name = username db.session.add(user) db.session.commit() - return redirect("/users") + return redirect("/user") -@app.route("/deleteuser", methods=["POST"]) -def deleteuser(): +@app.route("/user-delete", methods=["POST"]) +def user_delete_post(): 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") + return redirect("/user") if username == "Select User": - return redirect("/users") + return redirect("/user") print(f"Deleting user {username}...") User.query.filter_by(name=username).delete() db.session.commit() - return redirect("/users") + return redirect("/user") if __name__ == "__main__": + app.url_map.strict_slashes = False app.run(debug=True, host="0.0.0.0") diff --git a/model.py b/model.py index 47cd8a2..1712aa0 100644 --- a/model.py +++ b/model.py @@ -1,10 +1,11 @@ -from typing import ClassVar, List, Dict +import json +from datetime import datetime +from typing import List, Dict +from urllib.parse import quote from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, PickleType +from sqlalchemy import Integer, String, DateTime, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship -from datetime import datetime -import json db = SQLAlchemy() @@ -21,18 +22,20 @@ class Race(db.Model): __tablename__ = "race" def from_csv(self, row): - self.id = int(row[0]) - self.grandprix = str(row[1]) - self.number = int(row[2]) - self.date = datetime.strptime(row[3], "%Y-%m-%d") - self.pxx = int(row[4]) # This is the place that has to be guessed for this race + self.name = str(row[0]) + self.number = int(row[1]) + self.date = datetime.strptime(row[2], "%Y-%m-%d") + self.pxx = int(row[3]) return self - id: Mapped[int] = mapped_column(Integer, primary_key=True) - grandprix: Mapped[str] = mapped_column(String(32)) + @property + def name_sanitized(self): + return quote(self.name) + + name: Mapped[str] = mapped_column(String(64), primary_key=True) number: Mapped[int] = mapped_column(Integer) date: Mapped[datetime] = mapped_column(DateTime) - pxx: Mapped[int] = mapped_column(Integer) + pxx: Mapped[int] = mapped_column(Integer) # This is the place to guess class Team(db.Model): @@ -58,17 +61,17 @@ class Driver(db.Model): def from_csv(self, row): self.name = str(row[0]) self.abbr = str(row[1]) - self.team_id = str(row[2]) + self.team_name = str(row[2]) self.country_code = str(row[3]) return self name: Mapped[str] = mapped_column(String(32), primary_key=True) abbr: Mapped[str] = mapped_column(String(3)) - team_id: Mapped[str] = mapped_column(ForeignKey("team.name")) + team_name: Mapped[str] = mapped_column(ForeignKey("team.name")) country_code: Mapped[str] = mapped_column(String(2)) # alpha-2 code # Relationships - team: Mapped["Team"] = relationship("Team", foreign_keys=[team_id]) + team: Mapped["Team"] = relationship("Team", foreign_keys=[team_name]) ###################################### @@ -92,6 +95,10 @@ class User(db.Model): self.name ] + @property + def name_sanitized(self): + return quote(self.name) + name: Mapped[str] = mapped_column(String(32), primary_key=True) @@ -101,113 +108,119 @@ class RaceResult(db.Model): 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" - __allow_unmapped__ = True - __csv_header__ = ["id", "race_id", "pxx_ids_json", "dnf_ids_json", "exclude_ids_json"] + __allow_unmapped__ = True # TODO: Used for json conversion, move this to some other class instead + __csv_header__ = ["race_name", "pxx_driver_names_json", "dnf_driver_names_json", "excluded_driver_names_json"] def from_csv(self, row): - self.id = int(row[0]) - self.race_id = int(row[1]) - self.pxx_ids_json = str(row[2]) - self.dnf_ids_json = str(row[3]) - self.exclude_ids_json = str(row[4]) + self.race_name = str(row[0]) + self.pxx_driver_names_json = str(row[1]) + self.dnf_driver_names_json = str(row[2]) + self.excluded_driver_names_json = str(row[3]) return self def to_csv(self): return [ - self.id, - self.race_id, - self.pxx_ids_json, - self.dnf_ids_json, - self.exclude_ids_json + self.race_name, + self.pxx_driver_names_json, + self.dnf_driver_names_json, + self.excluded_driver_names_json ] - id: Mapped[int] = mapped_column(Integer, primary_key=True) - race_id: Mapped[int] = mapped_column(ForeignKey("race.id")) - pxx_ids_json: Mapped[str] = mapped_column(String(1024)) - dnf_ids_json: Mapped[str] = mapped_column(String(1024)) - exclude_ids_json: Mapped[str] = mapped_column(String(1024)) + race_name: Mapped[str] = mapped_column(ForeignKey("race.name"), primary_key=True) + pxx_driver_names_json: Mapped[str] = mapped_column(String(1024)) + dnf_driver_names_json: Mapped[str] = mapped_column(String(1024)) + excluded_driver_names_json: Mapped[str] = mapped_column(String(1024)) @property - def pxx_ids(self) -> Dict[str, str]: - return json.loads(self.pxx_ids_json) + def pxx_driver_names(self) -> Dict[str, str]: + return json.loads(self.pxx_driver_names_json) - @pxx_ids.setter - def pxx_ids(self, new_pxx_ids: Dict[str, str]): - self.pxx_ids_json = json.dumps(new_pxx_ids) + @pxx_driver_names.setter + def pxx_driver_names(self, new_pxx_driver_names: Dict[str, str]): + self.pxx_driver_names_json = json.dumps(new_pxx_driver_names) @property - def dnf_ids(self) -> Dict[str, str]: - return json.loads(self.dnf_ids_json) + def dnf_driver_names(self) -> Dict[str, str]: + return json.loads(self.dnf_driver_names_json) - @dnf_ids.setter - def dnf_ids(self, new_dnf_ids: Dict[str, str]): - self.dnf_ids_json = json.dumps(new_dnf_ids) + @dnf_driver_names.setter + def dnf_driver_names(self, new_dnf_driver_names: Dict[str, str]): + self.dnf_driver_names_json = json.dumps(new_dnf_driver_names) @property - def exclude_ids(self) -> List[str]: - return json.loads(self.exclude_ids_json) + def excluded_driver_names(self) -> List[str]: + return json.loads(self.excluded_driver_names_json) - @exclude_ids.setter - def exclude_ids(self, new_exclude_ids: List[str]): - self.exclude_ids_json = json.dumps(new_exclude_ids) + @excluded_driver_names.setter + def excluded_driver_names(self, new_excluded_driver_names: List[str]): + self.excluded_driver_names_json = json.dumps(new_excluded_driver_names) # Relationships - race: Mapped["Race"] = relationship("Race", foreign_keys=[race_id]) - _pxxs: Dict[str, Driver] | None = None - _dnfs: Dict[str, Driver] | None = None - _excludes: List[Driver] | None = None + race: Mapped["Race"] = relationship("Race", foreign_keys=[race_name]) + _pxx_drivers: Dict[str, Driver] | None = None + _dnf_drivers: Dict[str, Driver] | None = None + _excluded_drivers: List[Driver] | None = None @property - def pxxs(self) -> Dict[str, Driver]: - if self._pxxs is None: - self._pxxs = dict() - for position, driver_id in self.pxx_ids.items(): - driver = Driver.query.filter_by(name=driver_id).first() + def pxx_drivers(self) -> Dict[str, Driver]: + if self._pxx_drivers is None: + self._pxx_drivers = dict() + for position, driver_name in self.pxx_driver_names.items(): + driver = Driver.query.filter_by(name=driver_name).first() if driver is None: - raise Exception(f"Error: Couldn't find driver with id {driver_id}") + raise Exception(f"Error: Couldn't find driver with id {driver_name}") - self._pxxs[position] = driver + self._pxx_drivers[position] = driver - return self._pxxs + return self._pxx_drivers @property - def dnfs(self) -> Dict[str, Driver]: - if self._dnfs is None: - self._dnfs = dict() - 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}") + def pxx_drivers_values(self) -> List[Driver]: + drivers: List[Driver] = [] - self._dnfs[position] = driver + # I don't know what order dict.values() etc. will return... + for position in range(1, 21): + drivers += [self.pxx_drivers[str(position)]] - return self._dnfs + return drivers @property - def excludes(self) -> List[Driver]: - if self._excludes is None: - self._excludes = [] - for driver_id in self.exclude_ids: - driver = Driver.query.filter_by(name=driver_id).first() + def dnf_drivers(self) -> Dict[str, Driver]: + if self._dnf_drivers is None: + self._dnf_drivers = dict() + for position, driver_name in self.dnf_driver_names.items(): + driver = Driver.query.filter_by(name=driver_name).first() if driver is None: - raise Exception(f"Error: Couldn't find driver with id {driver_id}") + raise Exception(f"Error: Couldn't find driver with id {driver_name}") - self._excludes += [driver] + self._dnf_drivers[position] = driver - return self._excludes + return self._dnf_drivers + + @property + def excluded_drivers(self) -> List[Driver]: + if self._excluded_drivers is None: + self._excluded_drivers = [] + for driver_name in self.excluded_driver_names: + driver = Driver.query.filter_by(name=driver_name).first() + if driver is None: + raise Exception(f"Error: Couldn't find driver with id {driver_name}") + + self._excluded_drivers += [driver] + + return self._excluded_drivers @property def pxx(self) -> Driver: pxx_num: str = str(self.race.pxx) - if pxx_num not in self.pxxs: - print(self.pxxs) + if pxx_num not in self.pxx_drivers: raise Exception(f"Error: Position {self.race.pxx} not contained in race result") - return self.pxxs[pxx_num] + return self.pxx_drivers[pxx_num] @property def dnf(self) -> Driver: - return sorted(self.dnfs.items(), reverse=True)[0][1] + return sorted(self.dnf_drivers.items(), reverse=True)[0][1] # SortedList[FirstElement][KeyPairValue] class RaceGuess(db.Model): @@ -216,36 +229,33 @@ class RaceGuess(db.Model): It stores the corresponding race and the guessed drivers for PXX and DNF. """ __tablename__ = "raceguess" - __csv_header__ = ["id", "user_id", "race_id", "pxx_id", "dnf_id"] + __csv_header__ = ["user_name", "race_name", "pxx_driver_name", "dnf_driver_name"] def from_csv(self, row): - self.id = int(row[0]) - self.user_id = str(row[1]) - self.race_id = int(row[2]) - self.pxx_id = str(row[3]) - self.dnf_id = str(row[4]) + self.user_name = str(row[0]) + self.race_name = str(row[1]) + self.pxx_driver_name = str(row[2]) + self.dnf_driver_name = str(row[3]) return self def to_csv(self): return [ - self.id, - self.user_id, - self.race_id, - self.pxx_id, - self.dnf_id + self.user_name, + self.race_name, + self.pxx_driver_name, + self.dnf_driver_name ] - id: Mapped[int] = mapped_column(Integer, primary_key=True) - user_id: Mapped[str] = mapped_column(ForeignKey("user.name")) - race_id: Mapped[int] = mapped_column(ForeignKey("race.id")) - pxx_id: Mapped[str] = mapped_column(ForeignKey("driver.name")) - dnf_id: Mapped[str] = mapped_column(ForeignKey("driver.name")) + user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True) + race_name: Mapped[str] = mapped_column(ForeignKey("race.name"), primary_key=True) + pxx_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name")) + dnf_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name")) # Relationships - user: Mapped["User"] = relationship("User", foreign_keys=[user_id]) - race: Mapped["Race"] = relationship("Race", foreign_keys=[race_id]) - pxx: Mapped["Driver"] = relationship("Driver", foreign_keys=[pxx_id]) - dnf: Mapped["Driver"] = relationship("Driver", foreign_keys=[dnf_id]) + user: Mapped["User"] = relationship("User", foreign_keys=[user_name]) + race: Mapped["Race"] = relationship("Race", foreign_keys=[race_name]) + pxx: Mapped["Driver"] = relationship("Driver", foreign_keys=[pxx_driver_name]) + dnf: Mapped["Driver"] = relationship("Driver", foreign_keys=[dnf_driver_name]) class TeamWinners(db.Model): @@ -254,50 +264,46 @@ class TeamWinners(db.Model): """ __tablename__ = "teamwinners" __allow_unmapped__ = True - __csv_header__ = ["id", "user_id", "winner_ids_json"] + __csv_header__ = ["user_name", "teamwinner_driver_names_json"] def from_csv(self, row): - self.id = int(row[0]) - self.user_id = str(row[1]) - self.winner_ids_json = str(row[2]) + self.user_name = str(row[0]) + self.teamwinner_driver_names_json = str(row[1]) return self def to_csv(self): return [ - self.id, - self.user_id, - self.winner_ids_json + self.user_name, + self.teamwinner_driver_names_json ] - id: Mapped[int] = mapped_column(Integer, primary_key=True) - user_id: Mapped[str] = mapped_column(ForeignKey("user.name")) - winner_ids_json: Mapped[str] = mapped_column(String(512)) + user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True) + teamwinner_driver_names_json: Mapped[str] = mapped_column(String(1024)) @property - def winner_ids(self) -> List[str]: - return json.loads(self.winner_ids_json) + def teamwinner_driver_names(self) -> List[str]: + return json.loads(self.teamwinner_driver_names_json) - @winner_ids.setter - def winner_ids(self, new_winner_ids: List[str]): - self.winner_ids_json = json.dumps(new_winner_ids) + @teamwinner_driver_names.setter + def teamwinner_driver_names(self, new_teamwinner_driver_names: List[str]): + self.teamwinner_driver_names_json = json.dumps(new_teamwinner_driver_names) # Relationships - user: Mapped["User"] = relationship("User", foreign_keys=[user_id]) - _winners: List[Driver] | None = None + user: Mapped["User"] = relationship("User", foreign_keys=[user_name]) + _teamwinner_drivers: List[Driver] | None = None @property - def winners(self) -> List[Driver]: - if self._winners is None: - self._winners = [] - for driver_id in self.winner_ids: - driver = Driver.query.filter_by(name=driver_id).first() + def teamwinners(self) -> List[Driver]: + if self._teamwinner_drivers is None: + self._teamwinner_drivers = [] + for driver_name in self.teamwinner_driver_names: + driver = Driver.query.filter_by(name=driver_name).first() if driver is None: - raise Exception(f"Error: Couldn't find driver with id {driver_id}") + raise Exception(f"Error: Couldn't find driver with id {driver_name}") - self._winners += [driver] + self._teamwinner_drivers += [driver] - - return self._winners + return self._teamwinner_drivers class PodiumDrivers(db.Model): @@ -306,49 +312,46 @@ class PodiumDrivers(db.Model): """ __tablename__ = "podiumdrivers" __allow_unmapped__ = True - __csv_header__ = ["id", "user_id", "podium_ids_json"] + __csv_header__ = ["user_name", "podium_driver_names_json"] def from_csv(self, row): - self.id = int(row[0]) - self.user_id = str(row[1]) - self.podium_ids_json = str(row[2]) + self.user_name = str(row[0]) + self.podium_driver_names_json = str(row[1]) return self def to_csv(self): return [ - self.id, - self.user_id, - self.podium_ids_json + self.user_name, + self.podium_driver_names_json ] - id: Mapped[int] = mapped_column(Integer, primary_key=True) - user_id: Mapped[str] = mapped_column(ForeignKey("user.name")) - podium_ids_json: Mapped[str] = mapped_column(String(512)) + user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True) + podium_driver_names_json: Mapped[str] = mapped_column(String(1024)) @property - def podium_ids(self) -> List[str]: - return json.loads(self.podium_ids_json) + def podium_driver_names(self) -> List[str]: + return json.loads(self.podium_driver_names_json) - @podium_ids.setter - def podium_ids(self, new_podium_ids: List[str]): - self.podium_ids_json = json.dumps(new_podium_ids) + @podium_driver_names.setter + def podium_driver_names(self, new_podium_driver_names: List[str]): + self.podium_driver_names_json = json.dumps(new_podium_driver_names) # Relationships - user: Mapped["User"] = relationship("User", foreign_keys=[user_id]) - _podiums: List[Driver] | None = None + user: Mapped["User"] = relationship("User", foreign_keys=[user_name]) + _podium_drivers: List[Driver] | None = None @property - def podiums(self) -> List[Driver]: - if self._podiums is None: - self._podiums = [] - for driver_id in self.podium_ids: - driver = Driver.query.filter_by(name=driver_id).first() + def podium_drivers(self) -> List[Driver]: + if self._podium_drivers is None: + self._podium_drivers = [] + for driver_name in self.podium_driver_names: + driver = Driver.query.filter_by(name=driver_name).first() if driver is None: - raise Exception(f"Error: Couldn't find driver with id {driver_id}") + raise Exception(f"Error: Couldn't find driver with id {driver_name}") - self._podiums += [driver] + self._podium_drivers += [driver] - return self._podiums + return self._podium_drivers class SeasonGuess(db.Model): @@ -356,54 +359,47 @@ class SeasonGuess(db.Model): A collection of bonus guesses for the entire season. """ __tablename__ = "seasonguess" - __csv_header__ = ["id", "user_id", - "hot_take", "p2_constructor_id", "most_overtakes_id", "most_dnfs_id", "most_gained_id", - "most_lost_id", "team_winners_id", "podium_drivers_id"] + __csv_header__ = ["user_name", "hot_take", "p2_team_name", + "overtake_driver_name", "dnf_driver_name", "gained_driver_name", "lost_driver_name"] def from_csv(self, row): - self.id = int(row[0]) - self.user_id = str(row[1]) - self.hot_take = str(row[2]) - self.p2_constructor_id = str(row[3]) - self.most_overtakes_id = str(row[4]) - self.most_dnfs_id = str(row[5]) - self.most_gained_id = str(row[6]) - self.most_lost_id = str(row[6]) - self.team_winners_id = int(row[8]) - self.podium_drivers_id = int(row[9]) + self.user_name = str(row[0]) # Also used as foreign key for teamwinners + podiumdrivers + self.hot_take = str(row[1]) + self.p2_team_name = str(row[2]) + self.overtake_driver_name = str(row[3]) + self.dnf_driver_name = str(row[4]) + self.gained_driver_name = str(row[5]) + self.lost_driver_name = str(row[6]) return self def to_csv(self): return [ - self.id, - self.user_id, + self.user_name, self.hot_take, - self.p2_constructor_id, - self.most_overtakes_id, - self.most_dnfs_id, - self.most_gained_id, - self.most_lost_id, - self.team_winners_id, - self.podium_drivers_id + self.p2_team_name, + self.overtake_driver_name, + self.dnf_driver_name, + self.gained_driver_name, + self.lost_driver_name, ] - id: Mapped[int] = mapped_column(Integer, primary_key=True) - user_id: Mapped[str] = mapped_column(ForeignKey("user.name")) + user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True) hot_take: Mapped[str] = mapped_column(String(512)) - p2_constructor_id: Mapped[str] = mapped_column(ForeignKey("team.name")) - most_overtakes_id: Mapped[str] = mapped_column(ForeignKey("driver.name")) - most_dnfs_id: Mapped[str] = mapped_column(ForeignKey("driver.name")) - most_gained_id: Mapped[str] = mapped_column(ForeignKey("driver.name")) - most_lost_id: Mapped[str] = mapped_column(ForeignKey("driver.name")) - team_winners_id: Mapped[int] = mapped_column(ForeignKey("teamwinners.id")) - podium_drivers_id: Mapped[int] = mapped_column(ForeignKey("podiumdrivers.id")) + p2_team_name: Mapped[str] = mapped_column(ForeignKey("team.name")) + overtake_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name")) + dnf_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name")) + gained_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name")) + lost_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name")) # Relationships - user: Mapped["User"] = relationship("User", foreign_keys=[user_id]) - p2_constructor: Mapped["Team"] = relationship("Team", foreign_keys=[p2_constructor_id]) - most_overtakes: Mapped["Driver"] = relationship("Driver", foreign_keys=[most_overtakes_id]) - most_dnfs: Mapped["Driver"] = relationship("Driver", foreign_keys=[most_dnfs_id]) - most_gained: Mapped["Driver"] = relationship("Driver", foreign_keys=[most_gained_id]) - most_lost: Mapped["Driver"] = relationship("Driver", foreign_keys=[most_lost_id]) - team_winners: Mapped["TeamWinners"] = relationship("TeamWinners", foreign_keys=[team_winners_id]) - podium_drivers: Mapped["PodiumDrivers"] = relationship("PodiumDrivers", foreign_keys=[podium_drivers_id]) + user: Mapped["User"] = relationship("User", foreign_keys=[user_name]) + p2_team: Mapped["Team"] = relationship("Team", foreign_keys=[p2_team_name]) + overtake_driver: Mapped["Driver"] = relationship("Driver", foreign_keys=[overtake_driver_name]) + dnf_driver: Mapped["Driver"] = relationship("Driver", foreign_keys=[dnf_driver_name]) + gained_driver: Mapped["Driver"] = relationship("Driver", foreign_keys=[gained_driver_name]) + lost_driver: Mapped["Driver"] = relationship("Driver", foreign_keys=[lost_driver_name]) + + team_winners: Mapped["TeamWinners"] = relationship("TeamWinners", foreign_keys=[user_name], + primaryjoin="SeasonGuess.user_name == TeamWinners.user_name") + podium_drivers: Mapped["PodiumDrivers"] = relationship("PodiumDrivers", foreign_keys=[user_name], + primaryjoin="SeasonGuess.user_name == PodiumDrivers.user_name") diff --git a/static_data/drivers.csv b/static_data/drivers.csv index 11f2246..a0f3e30 100644 --- a/static_data/drivers.csv +++ b/static_data/drivers.csv @@ -1,4 +1,4 @@ -name,abbr,team_id,country_code +name,abbr,team_name,country_code NONE,NON,NONE,NO Alexander Albon,ALB,Williams,TH Fernando Alonso,ALO,Aston Martin,ES diff --git a/static_data/races.csv b/static_data/races.csv index 1f9d58f..c799738 100644 --- a/static_data/races.csv +++ b/static_data/races.csv @@ -1,24 +1,24 @@ -id,grandprix,number,date,pxx -1,Bahrain,1,2023-03-05,3 -2,Saudi Arabia,2,2023-03-19,17 -3,Melbourne,3,2023-04-02,5 -4,Baku,4,2023-04-30,6 -5,Miami,5,2023-05-07,15 -6,Imola,6,2023-05-21,8 -7,Monaco,7,2023-05-28,9 -8,Barcelona,8,2023-06-04,13 -9,Montreal,9,2023-06-18,11 -10,Spielberg,10,2023-07-02,12 -11,Silverstone,11,2023-07-09,18 -12,Budapest,12,2023-07-23,12 -13,Spa,13,2023-07-30,13 -14,Zandvoort,14,2023-08-27,3 -15,Monza,15,2023-09-03,6 -16,Singapore,16,2023-09-17,10 -17,Suzuka,17,2023-09-24,11 -18,Qatar,18,2023-10-08,3 -19,Austin,19,2023-10-22,11 -20,Mexico,20,2023-10-29,17 -21,Brazil,21,2023-11-05,14 -22,Las Vegas,22,2023-11-18,8 -23,Abu Dhabi,23,2023-11-26,5 \ No newline at end of file +name,number,date,pxx +Bahrain,1,2023-03-05,3 +Saudi Arabia,2,2023-03-19,17 +Melbourne,3,2023-04-02,5 +Baku,4,2023-04-30,6 +Miami,5,2023-05-07,15 +Imola,6,2023-05-21,8 +Monaco,7,2023-05-28,9 +Barcelona,8,2023-06-04,13 +Montreal,9,2023-06-18,11 +Spielberg,10,2023-07-02,12 +Silverstone,11,2023-07-09,18 +Budapest,12,2023-07-23,12 +Spa,13,2023-07-30,13 +Zandvoort,14,2023-08-27,3 +Monza,15,2023-09-03,6 +Singapore,16,2023-09-17,10 +Suzuka,17,2023-09-24,11 +Qatar,18,2023-10-08,3 +Austin,19,2023-10-22,11 +Mexico,20,2023-10-29,17 +Brazil,21,2023-11-05,14 +Las Vegas,22,2023-11-18,8 +Abu Dhabi,23,2023-11-26,5 \ No newline at end of file diff --git a/static_data/teams.csv b/static_data/teams.csv index 189776c..c1fa266 100644 --- a/static_data/teams.csv +++ b/static_data/teams.csv @@ -1,4 +1,4 @@ -team +name Alpine Aston Martin Ferrari diff --git a/template_model.py b/template_model.py new file mode 100644 index 0000000..5cae141 --- /dev/null +++ b/template_model.py @@ -0,0 +1,303 @@ +from typing import List, Iterable, Callable, TypeVar, Dict, overload, Any + +from sqlalchemy import desc + +from model import User, RaceResult, RaceGuess, Race, Driver, Team, SeasonGuess + +_T = TypeVar("_T") + + +def find_first_or_none(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T | None: + """ + Finds the first element in a sequence matching a predicate. + Returns None if no element is found. + """ + return next(filter(predicate, iterable), None) + + +def find_multiple(predicate: Callable[[_T], bool], iterable: Iterable[_T], count: int = 0) -> List[_T]: + """ + Finds elements in a sequence matching a predicate (finds all if is 0). + Throws exception if more/fewer elements were found than specified. + """ + filtered = list(filter(predicate, iterable)) + + if count != 0 and len(filtered) != count: + raise Exception(f"find_multiple found {len(filtered)} matching elements but expected {count}") + + return filtered + + +def find_single(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T: + """ + Find a single element in a sequence matching a predicate. + Throws exception if more/less than a single element is found. + """ + filtered = list(filter(predicate, iterable)) + + if len(filtered) != 1: + raise Exception(f"find_single found {len(filtered)} matching elements but expected 1") + + return filtered[0] + + +def find_single_or_none(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T | None: + """ + Find a single element in a sequence matching a predicate if it exists. + Only throws exception if more than a single element is found. + """ + filtered = list(filter(predicate, iterable)) + + if len(filtered) > 1: + raise Exception(f"find_single_or_none found {len(list(filtered))} matching elements but expected 0 or 1") + + return filtered[0] if len(filtered) == 1 else None + + +# This is inefficient (doesn't matter for small database), but very simple +class TemplateModel: + """ + This class bundles all data required from inside a template. + """ + + _all_users: List[User] | None = None + _all_race_results: List[RaceResult] | None = None + _all_race_guesses: List[RaceGuess] | None = None + _all_season_guesses: List[SeasonGuess] | None = None + _all_races: List[Race] | None = None + _all_drivers: List[Driver] | None = None + _all_teams: List[Team] | None = None + + def all_users(self) -> List[User]: + """ + Returns a list of all users in the database. + """ + if self._all_users is None: + self._all_users = User.query.all() + + return self._all_users + + @overload + def user_by(self, *, user_name: str) -> User: + """ + Tries to obtain the user object for a specific username. + """ + return self.user_by(user_name=user_name) + + @overload + def user_by(self, *, user_name: str, ignore: List[str]) -> User | None: + """ + Tries to obtain the user object for a specific username, but ignores certain usernames. + """ + return self.user_by(user_name=user_name, ignore=ignore) + + def user_by(self, *, user_name: str, ignore: List[str] | None = None) -> User | None: + if ignore is None: + ignore = [] + + if len(ignore) > 0 and user_name in ignore: + return None + + predicate: Callable[[User], bool] = lambda user: user.name == user_name + return find_single(predicate, self.all_users()) + + def all_race_results(self) -> List[RaceResult]: + """ + Returns a list of all race results in the database, in descending order (most recent first). + """ + if self._all_race_results is None: + self._all_race_results = RaceResult.query.join(RaceResult.race).order_by(desc(Race.number)).all() + + print(self._all_race_results) + return self._all_race_results + + def race_result_by(self, *, race_name: str) -> RaceResult | None: + """ + Tries to obtain the race result corresponding to a race name. + """ + predicate: Callable[[RaceResult], bool] = lambda result: result.race.name == race_name + return find_single_or_none(predicate, self.all_race_results()) + + def all_race_guesses(self) -> List[RaceGuess]: + """ + Returns a list of all race guesses in the database. + """ + if self._all_race_guesses is None: + self._all_race_guesses = RaceGuess.query.all() + + return self._all_race_guesses + + @overload + def race_guesses_by(self, *, user_name) -> List[RaceGuess]: + """ + Returns a list of all race guesses made by a specific user. + """ + return self.race_guesses_by(user_name=user_name) + + @overload + def race_guesses_by(self, *, race_name) -> List[RaceGuess]: + """ + Returns a list of all race guesses made for a specific race. + """ + return self.race_guesses_by(race_name=race_name) + + @overload + def race_guesses_by(self, *, user_name, race_name) -> RaceGuess | None: + """ + Returns a single race guess by a specific user for a specific race, or None, if this guess doesn't exist. + """ + return self.race_guesses_by(user_name=user_name, race_name=race_name) + + @overload + def race_guesses_by(self) -> Dict[str, Dict[str, RaceGuess]]: + """ + Returns a dictionary that maps race-ids to user-id - guess dictionaries. + """ + return self.race_guesses_by() + + def race_guesses_by(self, *, user_name=None, race_name=None) -> RaceGuess | List[RaceGuess] | Dict[str, Dict[str, RaceGuess]] | None: + # List of all guesses by a single user + if user_name is not None and race_name is None: + predicate: Callable[[RaceGuess], bool] = lambda guess: guess.user_name == user_name + return find_multiple(predicate, self.all_race_guesses()) + + # List of all guesses for a single race + if user_name is None and race_name is not None: + predicate: Callable[[RaceGuess], bool] = lambda guess: guess.race_name == race_name + return find_multiple(predicate, self.all_race_guesses()) + + # Guess for a single race by a single user + if user_name is not None and race_name is not None: + predicate: Callable[[RaceGuess], bool] = lambda guess: guess.user_name == user_name and guess.race_name == race_name + return find_single_or_none(predicate, self.all_race_guesses()) + + # Dict with all guesses + if user_name is None and race_name is None: + guesses_by: Dict[str, Dict[str, RaceGuess]] = dict() + guess: RaceGuess + + for guess in self.all_race_guesses(): + if guess.race_name not in guesses_by: + guesses_by[guess.race_name] = dict() + + guesses_by[guess.race_name][guess.user_name] = guess + + return guesses_by + + raise Exception("race_guesses_by encountered illegal combination of arguments") + + def all_season_guesses(self) -> List[SeasonGuess]: + if self._all_season_guesses is None: + self._all_season_guesses = SeasonGuess.query.all() + + return self._all_season_guesses + + @overload + def season_guesses_by(self, *, user_name) -> SeasonGuess: + """ + Returns the season guess made by a specific user. + """ + return self.season_guesses_by(user_name=user_name) + + @overload + def season_guesses_by(self) -> Dict[str, SeasonGuess]: + """ + Returns a dictionary of season guesses mapped to usernames. + """ + return self.season_guesses_by() + + def season_guesses_by(self, *, user_name=None) -> SeasonGuess | Dict[str, SeasonGuess] | None: + if user_name is not None: + predicate: Callable[[SeasonGuess], bool] = lambda guess: guess.user_name == user_name + return find_single_or_none(predicate, self.all_season_guesses()) + + if user_name is None: + guesses_by: Dict[str, SeasonGuess] = dict() + guess: SeasonGuess + + for guess in self.all_season_guesses(): + guesses_by[guess.user_name] = guess + + return guesses_by + + raise Exception("season_guesses_by encountered illegal combination of arguments") + + def all_races(self) -> List[Race]: + """ + Returns a list of all races in the database. + """ + if self._all_races is None: + self._all_races = Race.query.order_by(desc(Race.number)).all() + + return self._all_races + + def first_race_without_result(self) -> Race | None: + """ + Returns the first race-object with no associated race result. + """ + results: List[RaceResult] = self.all_race_results() + if len(results) == 0: + return self.all_races()[-1] # all_races is sorted descending by number + + most_recent_result: RaceResult = results[0] + predicate: Callable[[Race], bool] = lambda race: race.number == most_recent_result.race.number + 1 + + return find_first_or_none(predicate, self.all_races()) + + def all_teams(self) -> List[Team]: + """ + Returns a list of all teams in the database. + """ + if self._all_teams is None: + self._all_teams = Team.query.all() + + return self._all_teams + + def all_drivers(self) -> List[Driver]: + """ + Returns a list of all drivers in the database, including the NONE driver. + """ + if self._all_drivers is None: + self._all_drivers = Driver.query.all() + + return self._all_drivers + + def all_drivers_except_none(self) -> List[Driver]: + """ + Returns a list of all drivers in the database, excluding the NONE driver. + """ + predicate: Callable[[Driver], bool] = lambda driver: driver.name != "NONE" + return find_multiple(predicate, self.all_drivers()) + + @overload + def drivers_by(self, *, team_name) -> List[Driver]: + """ + Returns a list of all drivers driving for a certain team. + """ + return self.drivers_by(team_name=team_name) + + @overload + def drivers_by(self) -> Dict[str, List[Driver]]: + """ + Returns a dictionary of drivers mapped to team names. + """ + return self.drivers_by() + + def drivers_by(self, *, team_name=None) -> List[Driver] | Dict[str, List[Driver]]: + if team_name is not None: + predicate: Callable[[Driver], bool] = lambda driver: driver.team.name == team_name + return find_multiple(predicate, self.all_drivers_except_none(), 2) + + if team_name is None: + drivers_by: Dict[str, List[Driver]] = dict() + driver: Driver + team: Team + + for team in self.all_teams(): + drivers_by[team.name] = [] + for driver in self.all_drivers_except_none(): + drivers_by[driver.team.name] += [driver] + + return drivers_by + + raise Exception("drivers_by encountered illegal combination of arguments") diff --git a/templates/base.jinja b/templates/base.jinja index afd23b7..e7ea405 100644 --- a/templates/base.jinja +++ b/templates/base.jinja @@ -2,10 +2,16 @@ {# Simple driver dropdown. Requires list of drivers. #} -{% macro driver_select(name='', label='') %} +{% macro driver_select(name='', label='', include_none=true) %}
{# Use namespace wrapper to persist scope between loop iterations #} {% set user_has_chosen = namespace(driverpre="false") %} + {% if include_none == true %} + {% set drivers = model.all_drivers() %} + {% else %} + {% set drivers = model.all_drivers_except_none() %} + {% endif %} + {% for driver in drivers %} {% if match == driver.abbr %} {% set user_has_chosen.driverpre = "true" %} @@ -44,7 +56,7 @@
@@ -59,7 +71,7 @@ {# Use namespace wrapper to persist scope between loop iterations #} {% set user_has_chosen = namespace(teampre="false") %} - {% for team in teams %} + {% for team in model.all_teams() %} {% if match == team.name %} {% set user_has_chosen.teampre = "true" %} @@ -80,6 +92,7 @@ {# Easy nav-bar entries. When a page sets the active_page variable, the current entry will be underlined #} {% macro nav_selector(page='', text='') %} {% if active_page == page %}{% endif %} {{ text }} + {# NOTE: This should be set at the top of each template #} {% if active_page == page %}{% endif %} {% endmacro %} @@ -98,7 +111,7 @@ {% block head_extra %}{% endblock head_extra %} {# {% endblock head_extra %} +{% set current_race = model.first_race_without_result() %} + {% block navbar_center %} - {% if results | length > 0 %} + + {% if model.all_race_results() | length > 0 %} {% endif %} + {% endblock navbar_center %} {% block body %} -
-
-
{% if activeresult is not none %}{{ activeresult.race.grandprix }}{% else %} - {{ race.grandprix }}{% endif %}
+
-
-
    - {% if activeresult is not none %} - {% for driver in activeresult.pxxs.values() %} -
  • - {{ driver.name }} +
    +
    -
    - {# Driver DNFed #} -
    - - -
    +
    {% if active_result is not none %}{{ active_result.race.name }}{% else %} + {{ current_race.name }}{% endif %}
    - {# Driver Excluded #} -
    - - -
    -
    + +
      + + {% if active_result is not none %} + {% set drivers = active_result.pxx_drivers_values %} + {% else %} + {% set drivers = model.all_drivers_except_none() %} + {% endif %} - {# Standing order #} - - {% endfor %} - {% else %} {% for driver in drivers %}
    • {{ driver.name }} @@ -80,7 +73,8 @@ {# Driver DNFed #}
      + id="dnf-{{ driver.name }}" name="dnf-drivers" + {% if (active_result is not none) and (driver in active_result.dnf_drivers.values()) %}checked{% endif %}>
      @@ -88,20 +82,22 @@ {# Driver Excluded #}
      - + id="exclude-{{ driver.name }}" name="exclude-drivers" + {% if (active_result is not none) and (driver in active_result.excluded_drivers) %}checked{% endif %}> +
    {# Standing order #}
  • {% endfor %} - {% endif %} -
+ - -
+ + +
diff --git a/templates/race.jinja b/templates/race.jinja index 42664fc..4b8d409 100644 --- a/templates/race.jinja +++ b/templates/race.jinja @@ -1,26 +1,32 @@ {% extends 'base.jinja' %} -{% set active_page = "/race/" ~ activeuser.name | default("Everyone") %} -{% set active_user = activeuser %} - {% block title %}Formula 10 - Race{% endblock title %} +{% set active_page = "/race" %} + {% block navbar_center %} - + {% if model.all_users() | length > 1 %} + + {% endif %} {% endblock navbar_center %} {% block body %} @@ -30,11 +36,9 @@ Race - {% if activeuser is none %} - Call - {% else %} - Call - {% endif %} + Call + Result @@ -46,35 +50,43 @@   - {% if activeuser is none %} - {% for user in users %} - {{ user.name }} + {% if active_user is none %} + {% for user in model.all_users() %} + + {{ user.name }} + {% endfor %} {% else %} - {{ activeuser.name }} + {{ active_user.name }} {% endif %}   + {% set current_race = model.first_race_without_result() %} + {# Current Result, only displayed for all users overview #} - {% if activeuser is none %} + {% if active_user is none %} + - {{ currentrace.id }}: {{ currentrace.grandprix }}
- Guess: P{{ currentrace.pxx }} + {{ current_race.number }}: {{ current_race.name }}
+ Guess: P{{ current_race.pxx }} - {% for user in users %} - - {% if (currentrace.id in guesses) and (user.name in guesses.get(currentrace.id)) %} - {% set pxx = guesses.get(currentrace.id).get(user.name).pxx.abbr %} - {% set dnf = guesses.get(currentrace.id).get(user.name).dnf.abbr %} + {% for user in model.all_users() %} + {% set user_guess = model.race_guesses_by(user_name=user.name, race_name=current_race.name) %} + + {% if user_guess is not none %}
    -
  • P{{ currentrace.pxx }}: {{ pxx }}
  • -
  • DNF: {{ dnf }}
  • +
  • + P{{ current_race.pxx }}: {{ user_guess.pxx.abbr }}
  • +
  • + DNF: {{ user_guess.dnf.abbr }}
+ {% else %} +   {% endif %} {% endfor %} @@ -84,24 +96,24 @@ {% endif %} {# Enter Guess, only displayed for single user focused view #} - {% if activeuser is not none %} + {% if active_user is not none %} - {{ currentrace.id }}: {{ currentrace.grandprix }}
- Guess: P{{ currentrace.pxx }} + {{ current_race.number }}: {{ current_race.name }}
+ Guess: P{{ current_race.pxx }} -
+ + {% set user_guess = model.race_guesses_by(user_name=active_user.name, race_name=current_race.name) %} + {# Driver PXX Select #} - {{ driver_select_with_preselect(guesses.get(currentrace.id).get(activeuser.name).pxx.abbr if (currentrace.id in guesses and activeuser.name in guesses.get(currentrace.id)) else "", - "pxxselect", "P" ~ currentrace.pxx ~ ":") }} + {{ driver_select_with_preselect(user_guess.pxx.abbr if user_guess is not none else "", "pxxselect", "P" ~ current_race.pxx ~ ":") }} + +
{# Driver DNF Select #} -
- {{ driver_select_with_preselect(guesses.get(currentrace.id).get(activeuser.name).dnf.abbr if (currentrace.id in guesses and activeuser.name in guesses.get(currentrace.id)) else "", - "dnfselect", "DNF:") }} -
+ {{ driver_select_with_preselect(user_guess.dnf.abbr if user_guess is not none else "", "dnfselect", "DNF:") }}
@@ -112,35 +124,40 @@ {% endif %} {# Past Race Results #} - {% for raceresult in raceresults %} + {% for past_result in model.all_race_results() %} - {{ raceresult.race.id }}: {{ raceresult.race.grandprix }}
- Guessed: P{{ raceresult.race.pxx }} + {{ past_result.race.number }}: {{ past_result.race.name }}
+ Guessed: P{{ past_result.race.pxx }} - {% if activeuser is not none %}{% set users = [activeuser] %}{% endif %} + {% if active_user is none %} + {% set users = model.all_users() %} + {% else %} + {% set users = [active_user] %} + {% endif %} {% for user in users %} - {% if (raceresult.race_id in guesses) and (user.name in guesses.get(raceresult.race_id)) %} - {% set pxx = guesses.get(raceresult.race_id).get(user.name).pxx.abbr %} - {% set dnf = guesses.get(raceresult.race_id).get(user.name).dnf.abbr %} + {% set user_guess = model.race_guesses_by(user_name=user.name, race_name=past_result.race.name) %} + {% if user_guess is not none %}
    -
  • - P{{ raceresult.race.pxx }}: {{ pxx }}
  • -
  • - DNF: {{ dnf }}
  • +
  • + P{{ past_result.race.pxx }}: {{ user_guess.pxx.abbr }}
  • +
  • + DNF: {{ user_guess.dnf.abbr }}
+ {% else %} +   {% endif %} {% endfor %}
    -
  • P{{ raceresult.race.pxx }}: {{ raceresult.pxx.abbr }}
  • -
  • - DNF: {{ raceresult.dnf.abbr }}
  • +
  • P{{ past_result.race.pxx }}: {{ past_result.pxx.abbr }}
  • +
  • + DNF: {{ past_result.dnf.abbr }}
diff --git a/templates/season.jinja b/templates/season.jinja index 02f2955..5cc21d7 100644 --- a/templates/season.jinja +++ b/templates/season.jinja @@ -1,82 +1,100 @@ {% extends 'base.jinja' %} -{% set active_page = "/season/" ~ activeuser.name | default("Everyone") %} -{% set active_user = activeuser %} - {% block title %}Formula 10 - Season{% endblock title %} +{% set active_page = "/season" %} + {% block navbar_center %} - + {% if model.all_users() | length > 1 %} + + {% endif %} {% endblock navbar_center %} {% block body %} -
- {% if activeuser is not none %}{% set users = [activeuser] %}{% endif %} + {% if active_user is none %} + {% set users = model.all_users() %} + {% else %} + {% set users = [active_user] %} + {% endif %} {% for user in users %}
- {% if activeuser is not none %} -
{{ user.name }}
+ {% if active_user is none %} + +
{{ user.name }}
+
{% else %} -
{{ user.name }}
+
{{ user.name }}
{% endif %} -
+ {% set user_guess = model.season_guesses_by(user_name=user.name) %} + + {# Hot Take #}
+ {% if user_guess is not none %} + style="height: 50px">{{ user_guess.hot_take }} + {% else %} + + {% endif %} +
{# P2 Constructor #}
- {{ team_select_with_preselect(guesses.get(user.name).p2_constructor.name if user.name in guesses else "", + {{ team_select_with_preselect(user_guess.p2_team.name if user_guess is not none else "", "p2select", "P2 in WCC:") }}
{# Most Overtakes + DNFs #}
- {{ driver_select_with_preselect(guesses.get(user.name).most_overtakes.abbr if user.name in guesses else "", - "overtakeselect", "Most overtakes:") }} - {{ driver_select_with_preselect(guesses.get(user.name).most_dnfs.abbr if user.name in guesses else "", - "dnfselect", "Most DNFs:") }} + {{ driver_select_with_preselect(user_guess.overtake_driver.abbr if user_guess is not none else "", + "overtakeselect", "Most overtakes:", false) }} + {{ driver_select_with_preselect(user_guess.dnf_driver.abbr if user_guess is not none else "", + "dnfselect", "Most DNFs:", false) }}
{# Most Gained + Lost #}
- {{ driver_select_with_preselect(guesses.get(user.name).most_gained.abbr if user.name in guesses else "", - "gainedselect", "Most WDC places gained:") }} - {{ driver_select_with_preselect(guesses.get(user.name).most_lost.abbr if user.name in guesses else "", - "lostselect", "Most WDC places lost:") }} + {{ driver_select_with_preselect(user_guess.gained_driver.abbr if user_guess is not none else "", + "gainedselect", "Most WDC places gained:", false) }} + {{ driver_select_with_preselect(user_guess.gained_driver.abbr if user_guess is not none else "", + "lostselect", "Most WDC places lost:", false) }}
{# Team-internal Winners #}
Teammate battle winners:
- {% for team in teams %} - {% set driver_a_name = driverpairs.get(team.name)[0].name %} - {% set driver_b_name = driverpairs.get(team.name)[1].name %} + {% for team in model.all_teams() %} + {% set driver_a_name = model.drivers_by(team_name=team.name)[0].name %} + {% set driver_b_name = model.drivers_by(team_name=team.name)[1].name %}
@@ -84,7 +102,7 @@ name="teamwinner-{{ team.name }}" id="teamwinner-{{ team.name }}-1-{{ user.name }}" value="{{ driver_a_name }}" - {% if (user.name in guesses) and (driver_a_name in guesses.get(user.name).team_winners.winner_ids) %}checked="checked"{% endif %}> + {% if (user_guess is not none) and (driver_a_name in user_guess.team_winners.teamwinner_driver_names) %}checked="checked"{% endif %}>
@@ -96,7 +114,7 @@ name="teamwinner-{{ team.name }}" id="teamwinner-{{ team.name }}-2-{{ user.name }}" value="{{ driver_b_name }}" - {% if (user.name in guesses) and (driver_b_name in guesses.get(user.name).team_winners.winner_ids) %}checked="checked"{% endif %}> + {% if (user_guess is not none) and (driver_b_name in user_guess.team_winners.teamwinner_driver_names) %}checked="checked"{% endif %}>
@@ -107,9 +125,9 @@ {# Drivers with Podiums #}
Drivers with podium(s):
- {% for team in teams %} - {% set driver_a_name = driverpairs.get(team.name)[0].name %} - {% set driver_b_name = driverpairs.get(team.name)[1].name %} + {% for team in model.all_teams() %} + {% set driver_a_name = model.drivers_by(team_name=team.name)[0].name %} + {% set driver_b_name = model.drivers_by(team_name=team.name)[1].name %}
@@ -117,7 +135,7 @@ name="podiumdrivers" id="podium-{{ driver_a_name }}-{{ user.name }}" value="{{ driver_a_name }}" - {% if (user.name in guesses) and (driver_a_name in guesses.get(user.name).podium_drivers.podium_ids) %}checked="checked"{% endif %}> + {% if (user_guess is not none) and (driver_a_name in user_guess.podium_drivers.podium_driver_names) %}checked="checked"{% endif %}>
@@ -129,7 +147,7 @@ name="podiumdrivers" id="podium-{{ driver_b_name }}-{{ user.name }}" value="{{ driver_b_name }}" - {% if (user.name in guesses) and (driver_b_name in guesses.get(user.name).podium_drivers.podium_ids) %}checked="checked"{% endif %}> + {% if (user_guess is not none) and (driver_b_name in user_guess.podium_drivers.podium_driver_names) %}checked="checked"{% endif %}>
diff --git a/templates/users.jinja b/templates/users.jinja index ea6a906..5590192 100644 --- a/templates/users.jinja +++ b/templates/users.jinja @@ -1,7 +1,6 @@ {% extends 'base.jinja' %} {% set active_page = "/users" %} -{% set active_user = none %} {% block title %}Formula 10 - Users{% endblock title %} @@ -11,7 +10,7 @@
Add User
- +
Delete user
- +