diff --git a/.gitignore b/.gitignore index 708d0e8..634ddc0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__ instance data/dynamic_export +data/dynamic_export diff --git a/data/dynamic_export/users.csv b/data/dynamic_export/users.csv deleted file mode 100644 index 2b15e00..0000000 --- a/data/dynamic_export/users.csv +++ /dev/null @@ -1,8 +0,0 @@ -name -Christoph -Angela Merkel -Xi Jinping -Donald Trump -Joe Biden -Henri -Vinzent diff --git a/data/static_import/teams.csv b/data/static_import/teams.csv index c1fa266..c6d4ea9 100644 --- a/data/static_import/teams.csv +++ b/data/static_import/teams.csv @@ -1,4 +1,5 @@ name +None Alpine Aston Martin Ferrari diff --git a/formula10/__init__.py b/formula10/__init__.py index 6b3fbc0..dea5be4 100644 --- a/formula10/__init__.py +++ b/formula10/__init__.py @@ -18,17 +18,12 @@ import formula10.controller.admin_controller # type: ignore # TODO # General -# Split the database model from the frontend-model/template-model/domain-model -# - Move most of the template logic into this -# - Allow exclusion of e.g. most-gained driver and other stuff - # - Choose "place to guess" late before the race? Make a page for this # - Rules page # - Make user order changeable using drag'n'drop? # - Show place when entering race result (would require updating the drag'n'drop code...) # - 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? # Statistics # - Auto calculate points diff --git a/formula10/controller/admin_controller.py b/formula10/controller/admin_controller.py index 029b9fd..8b9e95a 100644 --- a/formula10/controller/admin_controller.py +++ b/formula10/controller/admin_controller.py @@ -3,8 +3,8 @@ from urllib.parse import unquote from flask import redirect, render_template, request from werkzeug import Response -from formula10.database.update_query_util import update_race_result, update_user -from formula10.database.import_export_util import export_dynamic_data, reload_dynamic_data, reload_static_data +from formula10.database.update_queries import update_race_result, update_user +from formula10.database.import_export import export_dynamic_data, reload_dynamic_data, reload_static_data from formula10.frontend.template_model import TemplateModel from formula10 import app @@ -42,10 +42,10 @@ def result_root() -> Response: @app.route("/result/") def result_active_race(race_name: str) -> str: race_name = unquote(race_name) - model = TemplateModel() - return render_template("enter.jinja", - active_result=model.race_result_by(race_name=race_name), - model=model) + model = TemplateModel(active_user_name=None, + active_result_race_name=race_name) + + return render_template("enter.jinja", model=model) @app.route("/result-enter/", methods=["POST"]) @@ -61,9 +61,10 @@ def result_enter_post(race_name: str) -> Response: @app.route("/user") def user_root() -> str: - model = TemplateModel() - return render_template("users.jinja", - model=model) + model = TemplateModel(active_user_name=None, + active_result_race_name=None) + + return render_template("users.jinja", model=model) @app.route("/user-add", methods=["POST"]) diff --git a/formula10/controller/race_controller.py b/formula10/controller/race_controller.py index 2e5d0de..99b890c 100644 --- a/formula10/controller/race_controller.py +++ b/formula10/controller/race_controller.py @@ -2,7 +2,7 @@ from urllib.parse import unquote from flask import redirect, render_template, request from werkzeug import Response -from formula10.database.update_query_util import delete_race_guess, update_race_guess +from formula10.database.update_queries import delete_race_guess, update_race_guess from formula10.frontend.template_model import TemplateModel from formula10 import app @@ -20,10 +20,10 @@ def race_root() -> Response: @app.route("/race/") def race_active_user(user_name: str) -> str: user_name = unquote(user_name) - model = TemplateModel() - return render_template("race.jinja", - active_user=model.user_by(user_name=user_name, ignore=["Everyone"]), - model=model) + model = TemplateModel(active_user_name=user_name, + active_result_race_name=None) + + return render_template("race.jinja", model=model) @app.route("/race-guess//", methods=["POST"]) diff --git a/formula10/controller/season_controller.py b/formula10/controller/season_controller.py index c349f12..92577f1 100644 --- a/formula10/controller/season_controller.py +++ b/formula10/controller/season_controller.py @@ -3,8 +3,8 @@ from urllib.parse import unquote from flask import redirect, render_template, request from werkzeug import Response -from formula10.database.model.team import Team -from formula10.database.update_query_util import update_season_guess +from formula10.database.model.db_team import DbTeam +from formula10.database.update_queries import update_season_guess from formula10.frontend.template_model import TemplateModel from formula10 import app, db @@ -17,10 +17,10 @@ def season_root() -> Response: @app.route("/season/") def season_active_user(user_name: str) -> str: user_name = unquote(user_name) - model = TemplateModel() - return render_template("season.jinja", - active_user=model.user_by(user_name=user_name, ignore=["Everyone"]), - model=model) + model = TemplateModel(active_user_name=user_name, + active_result_race_name=None) + + return render_template("season.jinja", model=model) @app.route("/season-guess/", methods=["POST"]) @@ -36,7 +36,7 @@ def season_guess_post(user_name: str) -> Response: ] # TODO: This is pretty ugly, to do queries in the controller team_winner_guesses: List[str | None] = [ - request.form.get(f"teamwinner-{team.name}") for team in db.session.query(Team).all() + request.form.get(f"teamwinner-{team.name}") for team in db.session.query(DbTeam).all() ] podium_driver_guesses: List[str] = request.form.getlist("podiumdrivers") diff --git a/formula10/database/common_queries.py b/formula10/database/common_queries.py new file mode 100644 index 0000000..e5f3db2 --- /dev/null +++ b/formula10/database/common_queries.py @@ -0,0 +1,23 @@ +from formula10.database.model.db_driver import DbDriver +from formula10.database.model.db_race_result import DbRaceResult +from formula10.database.model.db_user import DbUser +from formula10 import db + +def race_has_result(race_name: str) -> bool: + return db.session.query(DbRaceResult).filter_by(race_name=race_name).first() is not None + + +def user_exists_and_enabled(user_name: str) -> bool: + return db.session.query(DbUser).filter_by(name=user_name, enabled=True).first() is not None + + +def user_exists_and_disabled(user_name: str) -> bool: + return db.session.query(DbUser).filter_by(name=user_name, enabled=False).first() is not None + + +def find_single_driver_strict(driver_name: str) -> DbDriver: + db_driver: DbDriver | None = db.session.query(DbDriver).filter_by(name=driver_name).first() + if db_driver is None: + raise Exception(f"Could not find driver with name {driver_name} in database") + + return db_driver \ No newline at end of file diff --git a/formula10/database/common_query_util.py b/formula10/database/common_query_util.py deleted file mode 100644 index 03775b6..0000000 --- a/formula10/database/common_query_util.py +++ /dev/null @@ -1,10 +0,0 @@ -from formula10.database.model.race_result import RaceResult -from formula10.database.model.user import User -from formula10 import db - -def race_has_result(race_name: str) -> bool: - return db.session.query(RaceResult).filter_by(race_name=race_name).first() is not None - - -def user_exists(user_name: str) -> bool: - return db.session.query(User).filter_by(name=user_name).first() is not None \ No newline at end of file diff --git a/formula10/database/import_export_util.py b/formula10/database/import_export.py similarity index 51% rename from formula10/database/import_export_util.py rename to formula10/database/import_export.py index 601afd3..8f57030 100644 --- a/formula10/database/import_export_util.py +++ b/formula10/database/import_export.py @@ -2,16 +2,14 @@ import csv import os.path from typing import List, Any -from formula10.database.model.driver import Driver -from formula10.database.model.podium_drivers import PodiumDrivers -from formula10.database.model.race import Race -from formula10.database.model.race_guess import RaceGuess -from formula10.database.model.race_result import RaceResult -from formula10.database.model.season_guess import SeasonGuess -from formula10.database.model.team import Team -from formula10.database.model.team_winners import TeamWinners -from formula10.database.model.user import User from formula10 import db +from formula10.database.model.db_driver import DbDriver +from formula10.database.model.db_race import DbRace +from formula10.database.model.db_race_guess import DbRaceGuess +from formula10.database.model.db_race_result import DbRaceResult +from formula10.database.model.db_season_guess import DbSeasonGuess +from formula10.database.model.db_team import DbTeam +from formula10.database.model.db_user import DbUser def load_csv(filename: str) -> List[List[str]]: @@ -44,17 +42,17 @@ def reload_static_data(): db.create_all() # Clear static data - db.session.query(Team).delete() - db.session.query(Driver).delete() - db.session.query(Race).delete() + db.session.query(DbTeam).delete() + db.session.query(DbDriver).delete() + db.session.query(DbRace).delete() # Reload static data for row in load_csv("data/static_import/teams.csv"): - db.session.add(Team.from_csv(row)) + db.session.add(DbTeam.from_csv(row)) for row in load_csv("data/static_import/drivers.csv"): - db.session.add(Driver.from_csv(row)) + db.session.add(DbDriver.from_csv(row)) for row in load_csv("data/static_import/races.csv"): - db.session.add(Race.from_csv(row)) + db.session.add(DbRace.from_csv(row)) db.session.commit() @@ -65,26 +63,20 @@ def reload_dynamic_data(): db.create_all() # Clear dynamic data - db.session.query(User).delete() - db.session.query(RaceResult).delete() - db.session.query(RaceGuess).delete() - db.session.query(TeamWinners).delete() - db.session.query(PodiumDrivers).delete() - db.session.query(SeasonGuess).delete() + db.session.query(DbUser).delete() + db.session.query(DbRaceResult).delete() + db.session.query(DbRaceGuess).delete() + db.session.query(DbSeasonGuess).delete() # Reload dynamic data for row in load_csv("data/dynamic_export/users.csv"): - db.session.add(User.from_csv(row)) + db.session.add(DbUser.from_csv(row)) for row in load_csv("data/dynamic_export/raceresults.csv"): - db.session.add(RaceResult.from_csv(row)) + db.session.add(DbRaceResult.from_csv(row)) for row in load_csv("data/dynamic_export/raceguesses.csv"): - db.session.add(RaceGuess.from_csv(row)) - for row in load_csv("data/dynamic_export/teamwinners.csv"): - db.session.add(TeamWinners.from_csv(row)) - for row in load_csv("data/dynamic_export/podiumdrivers.csv"): - db.session.add(PodiumDrivers.from_csv(row)) + db.session.add(DbRaceGuess.from_csv(row)) for row in load_csv("data/dynamic_export/seasonguesses.csv"): - db.session.add(SeasonGuess.from_csv(row)) + db.session.add(DbSeasonGuess.from_csv(row)) db.session.commit() @@ -92,16 +84,12 @@ def reload_dynamic_data(): def export_dynamic_data(): print("Exporting Userdata...") - users: List[User] = db.session.query(User).all() - raceresults: List[RaceResult] = db.session.query(RaceResult).all() - raceguesses: List[RaceGuess] = db.session.query(RaceGuess).all() - teamwinners: List[TeamWinners] = db.session.query(TeamWinners).all() - podiumdrivers: List[PodiumDrivers] = db.session.query(PodiumDrivers).all() - seasonguesses: List[SeasonGuess] = db.session.query(SeasonGuess).all() + users: List[DbUser] = db.session.query(DbUser).all() + raceresults: List[DbRaceResult] = db.session.query(DbRaceResult).all() + raceguesses: List[DbRaceGuess] = db.session.query(DbRaceGuess).all() + seasonguesses: List[DbSeasonGuess] = db.session.query(DbSeasonGuess).all() write_csv("data/dynamic_export/users.csv", users) write_csv("data/dynamic_export/raceresults.csv", raceresults) write_csv("data/dynamic_export/raceguesses.csv", raceguesses) - write_csv("data/dynamic_export/teamwinners.csv", teamwinners) - write_csv("data/dynamic_export/podiumdrivers.csv", podiumdrivers) write_csv("data/dynamic_export/seasonguesses.csv", seasonguesses) diff --git a/formula10/database/model/driver.py b/formula10/database/model/db_driver.py similarity index 53% rename from formula10/database/model/driver.py rename to formula10/database/model/db_driver.py index fd913dd..21cb85d 100644 --- a/formula10/database/model/driver.py +++ b/formula10/database/model/db_driver.py @@ -2,25 +2,27 @@ from typing import List from sqlalchemy import String, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from formula10.database.model.team import Team +from formula10.database.model.db_team import DbTeam from formula10 import db -class Driver(db.Model): +class DbDriver(db.Model): """ A F1 driver. It stores the corresponding team + name abbreviation. """ __tablename__ = "driver" - @staticmethod - def from_csv(row: List[str]): - driver: Driver = Driver() - driver.name = str(row[0]) - driver.abbr = str(row[1]) - driver.team_name = str(row[2]) - driver.country_code = str(row[3]) - return driver + def __init__(self, *, name: str): + self.name = name # Primary key + + @classmethod + def from_csv(cls, row: List[str]): + db_driver: DbDriver = cls(name=str(row[0])) + db_driver.abbr = str(row[1]) + db_driver.team_name = str(row[2]) + db_driver.country_code = str(row[3]) + return db_driver name: Mapped[str] = mapped_column(String(32), primary_key=True) abbr: Mapped[str] = mapped_column(String(4)) @@ -28,4 +30,4 @@ class Driver(db.Model): country_code: Mapped[str] = mapped_column(String(2)) # alpha-2 code # Relationships - team: Mapped["Team"] = relationship("Team", foreign_keys=[team_name]) \ No newline at end of file + team: Mapped[DbTeam] = relationship("DbTeam", foreign_keys=[team_name]) \ No newline at end of file diff --git a/formula10/database/model/race.py b/formula10/database/model/db_race.py similarity index 53% rename from formula10/database/model/race.py rename to formula10/database/model/db_race.py index 704a513..de2efcd 100644 --- a/formula10/database/model/race.py +++ b/formula10/database/model/db_race.py @@ -1,31 +1,32 @@ from datetime import datetime from typing import List -from urllib.parse import quote from sqlalchemy import DateTime, Integer, String from sqlalchemy.orm import Mapped, mapped_column from formula10 import db -class Race(db.Model): +class DbRace(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" - @staticmethod - def from_csv(row: List[str]): - race: Race = Race() - race.name = str(row[0]) - race.number = int(row[1]) - race.date = datetime.strptime(row[2], "%Y-%m-%d") - race.pxx = int(row[3]) - return race + def __init__(self, *, name: str, number: int, date: datetime, pxx: int): + self.name = name # Primary key - @property - def name_sanitized(self) -> str: - return quote(self.name) + self.number = number + self.date = date + self.pxx = pxx + + @classmethod + def from_csv(cls, row: List[str]): + db_race: DbRace = cls(name=str(row[0]), + number=int(row[1]), + date=datetime.strptime(row[2], "%Y-%m-%d"), + pxx=int(row[3])) + return db_race name: Mapped[str] = mapped_column(String(64), primary_key=True) number: Mapped[int] = mapped_column(Integer) diff --git a/formula10/database/model/db_race_guess.py b/formula10/database/model/db_race_guess.py new file mode 100644 index 0000000..3aad55f --- /dev/null +++ b/formula10/database/model/db_race_guess.py @@ -0,0 +1,51 @@ +from typing import Any, List +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from formula10.database.model.db_user import DbUser +from formula10.database.model.db_race import DbRace +from formula10.database.model.db_driver import DbDriver +from formula10 import db + + +class DbRaceGuess(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" + __csv_header__ = ["user_name", "race_name", "pxx_driver_name", "dnf_driver_name"] + + def __init__(self, *, user_name: str, race_name: str, pxx_driver_name: str, dnf_driver_name: str): + self.user_name = user_name # Primary key + self.race_name = race_name # Primary key + + self.dnf_driver_name = dnf_driver_name + self.pxx_driver_name = pxx_driver_name + + @classmethod + def from_csv(cls, row: List[str]): + db_race_guess: DbRaceGuess = cls(user_name=str(row[0]), + race_name=str(row[1]), + pxx_driver_name=str(row[2]), + dnf_driver_name=str(row[3])) + return db_race_guess + + def to_csv(self) -> List[Any]: + return [ + self.user_name, + self.race_name, + self.pxx_driver_name, + self.dnf_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[DbUser] = relationship("DbUser", foreign_keys=[user_name]) + race: Mapped[DbRace] = relationship("DbRace", foreign_keys=[race_name]) + pxx: Mapped[DbDriver] = relationship("DbDriver", foreign_keys=[pxx_driver_name]) + dnf: Mapped[DbDriver] = relationship("DbDriver", foreign_keys=[dnf_driver_name]) \ No newline at end of file diff --git a/formula10/database/model/db_race_result.py b/formula10/database/model/db_race_result.py new file mode 100644 index 0000000..ba14237 --- /dev/null +++ b/formula10/database/model/db_race_result.py @@ -0,0 +1,49 @@ +from typing import Any, List +from sqlalchemy import ForeignKey, String +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from formula10.database.model.db_race import DbRace +from formula10 import db + +class DbRaceResult(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" + __csv_header__ = ["race_name", "pxx_driver_names_json", "first_dnf_driver_names_json", "dnf_driver_names_json", "excluded_driver_names_json"] + + def __init__(self, *, race_name: str, pxx_driver_names_json: str, first_dnf_driver_names_json: str, dnf_driver_names_json: str, excluded_driver_names_json: str): + self.race_name = race_name # Primary key + + self.pxx_driver_names_json = pxx_driver_names_json + self.first_dnf_driver_names_json = first_dnf_driver_names_json + self.dnf_driver_names_json = dnf_driver_names_json + self.excluded_driver_names_json = excluded_driver_names_json + + @classmethod + def from_csv(cls, row: List[str]): + db_race_result: DbRaceResult = cls(race_name=str(row[0]), + pxx_driver_names_json=str(row[1]), + first_dnf_driver_names_json=str(row[2]), + dnf_driver_names_json=str(row[3]), + excluded_driver_names_json=str(row[4])) + return db_race_result + + def to_csv(self) -> List[Any]: + return [ + self.race_name, + self.pxx_driver_names_json, + self.first_dnf_driver_names_json, + self.dnf_driver_names_json, + self.excluded_driver_names_json + ] + + race_name: Mapped[str] = mapped_column(ForeignKey("race.name"), primary_key=True) + pxx_driver_names_json: Mapped[str] = mapped_column(String(1024)) + first_dnf_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)) + + # Relationships + race: Mapped[DbRace] = relationship("DbRace", foreign_keys=[race_name]) \ No newline at end of file diff --git a/formula10/database/model/db_season_guess.py b/formula10/database/model/db_season_guess.py new file mode 100644 index 0000000..519c7d7 --- /dev/null +++ b/formula10/database/model/db_season_guess.py @@ -0,0 +1,67 @@ +from typing import Any, List +from sqlalchemy import ForeignKey, String +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from formula10.database.model.db_driver import DbDriver +from formula10.database.model.db_team import DbTeam +from formula10.database.model.db_user import DbUser +from formula10 import db + +class DbSeasonGuess(db.Model): + """ + A collection of bonus guesses for the entire season. + """ + __tablename__ = "seasonguess" + __csv_header__ = ["user_name", "hot_take", "p2_team_name", + "overtake_driver_name", "dnf_driver_name", "gained_driver_name", "lost_driver_name", + "team_winners_driver_names_json", "podium_drivers_driver_names_json"] + + def __init__(self, *, user_name: str, team_winners_driver_names_json: str, podium_drivers_driver_names_json: str): + self.user_name = user_name # Primary key + + self.team_winners_driver_names_json = team_winners_driver_names_json + self.podium_drivers_driver_names_json = podium_drivers_driver_names_json + + @classmethod + def from_csv(cls, row: List[str]): + db_season_guess: DbSeasonGuess = cls(user_name=str(row[0]), + team_winners_driver_names_json=str(row[7]), + podium_drivers_driver_names_json=str(row[8])) + db_season_guess.hot_take = str(row[1]) + db_season_guess.p2_team_name = str(row[2]) + db_season_guess.overtake_driver_name = str(row[3]) + db_season_guess.dnf_driver_name = str(row[4]) + db_season_guess.gained_driver_name = str(row[5]) + db_season_guess.lost_driver_name = str(row[6]) + return db_season_guess + + def to_csv(self) -> List[Any]: + return [ + self.user_name, + self.hot_take, + self.p2_team_name, + self.overtake_driver_name, + self.dnf_driver_name, + self.gained_driver_name, + self.lost_driver_name, + self.team_winners_driver_names_json, + self.podium_drivers_driver_names_json + ] + + user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True) + hot_take: Mapped[str | None] = mapped_column(String(512), nullable=True) + p2_team_name: Mapped[str | None] = mapped_column(ForeignKey("team.name"), nullable=True) + overtake_driver_name: Mapped[str | None] = mapped_column(ForeignKey("driver.name"), nullable=True) + dnf_driver_name: Mapped[str | None] = mapped_column(ForeignKey("driver.name"), nullable=True) + gained_driver_name: Mapped[str | None] = mapped_column(ForeignKey("driver.name"), nullable=True) + lost_driver_name: Mapped[str | None] = mapped_column(ForeignKey("driver.name"), nullable=True) + team_winners_driver_names_json: Mapped[str] = mapped_column(String(1024)) + podium_drivers_driver_names_json: Mapped[str] = mapped_column(String(1024)) + + # Relationships + user: Mapped[DbUser] = relationship("DbUser", foreign_keys=[user_name]) + p2_team: Mapped[DbTeam | None] = relationship("DbTeam", foreign_keys=[p2_team_name]) + overtake_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[overtake_driver_name]) + dnf_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[dnf_driver_name]) + gained_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[gained_driver_name]) + lost_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[lost_driver_name]) \ No newline at end of file diff --git a/formula10/database/model/team.py b/formula10/database/model/db_team.py similarity index 54% rename from formula10/database/model/team.py rename to formula10/database/model/db_team.py index 79506f4..528413c 100644 --- a/formula10/database/model/team.py +++ b/formula10/database/model/db_team.py @@ -4,16 +4,19 @@ from sqlalchemy.orm import Mapped, mapped_column from formula10 import db -class Team(db.Model): + +class DbTeam(db.Model): """ A constructor/team (name only). """ __tablename__ = "team" - @staticmethod - def from_csv(row: List[str]): - team: Team = Team() - team.name = str(row[0]) - return team + def __init__(self, *, name: str): + self.name = name # Primary key + + @classmethod + def from_csv(cls, row: List[str]): + db_team: DbTeam = cls(name=str(row[0])) + return db_team name: Mapped[str] = mapped_column(String(32), primary_key=True) \ No newline at end of file diff --git a/formula10/database/model/db_user.py b/formula10/database/model/db_user.py new file mode 100644 index 0000000..ae7bff8 --- /dev/null +++ b/formula10/database/model/db_user.py @@ -0,0 +1,32 @@ +from typing import Any, List +from sqlalchemy import Boolean, String +from sqlalchemy.orm import Mapped, mapped_column + +from formula10 import db + + +class DbUser(db.Model): + """ + A user that can guess races (name only). + """ + __tablename__ = "user" + __csv_header__ = ["name", "enabled"] + + def __init__(self, *, name: str, enabled: bool): + self.name = name # Primary key + + self.enabled = enabled + + @classmethod + def from_csv(cls, row: List[str]): + db_user: DbUser = cls(name=str(row[0]), enabled=bool(row[1])) + return db_user + + def to_csv(self) -> List[Any]: + return [ + self.name, + self.enabled + ] + + name: Mapped[str] = mapped_column(String(32), primary_key=True) + enabled: Mapped[bool] = mapped_column(Boolean) \ No newline at end of file diff --git a/formula10/database/model/podium_drivers.py b/formula10/database/model/podium_drivers.py deleted file mode 100644 index b1ae4b0..0000000 --- a/formula10/database/model/podium_drivers.py +++ /dev/null @@ -1,60 +0,0 @@ -import json -from typing import Any, List -from sqlalchemy import ForeignKey, String -from sqlalchemy.orm import Mapped, mapped_column, relationship - -from formula10.database.model.user import User -from formula10.database.model.driver import Driver -from formula10 import db - - -class PodiumDrivers(db.Model): - """ - A guessed list of each driver that will reach at least a single podium. - """ - __tablename__ = "podiumdrivers" - __allow_unmapped__ = True - __csv_header__ = ["user_name", "podium_driver_names_json"] - - def __init__(self, user_name: str): - self.user_name = user_name - - @staticmethod - def from_csv(row: List[str]): - podium_drivers: PodiumDrivers = PodiumDrivers(str(row[0])) - podium_drivers.podium_driver_names_json = str(row[1]) - return podium_drivers - - def to_csv(self) -> List[Any]: - return [ - self.user_name, - self.podium_driver_names_json - ] - - user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True) - podium_driver_names_json: Mapped[str] = mapped_column(String(1024), nullable=True) - - @property - def podium_driver_names(self) -> List[str]: - return json.loads(self.podium_driver_names_json) - - @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_name]) - _podium_drivers: List[Driver] | None = None - - @property - def podium_drivers(self) -> List[Driver]: - if self._podium_drivers is None: - self._podium_drivers = list() - for driver_name in self.podium_driver_names: - driver: Driver | None = db.session.query(Driver).filter_by(name=driver_name).first() - if driver is None: - raise Exception(f"Error: Couldn't find driver with id {driver_name}") - - self._podium_drivers.append(driver) - - return self._podium_drivers \ No newline at end of file diff --git a/formula10/database/model/race_guess.py b/formula10/database/model/race_guess.py deleted file mode 100644 index 7d2e90f..0000000 --- a/formula10/database/model/race_guess.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import Any, List -from sqlalchemy import ForeignKey -from sqlalchemy.orm import Mapped, mapped_column, relationship - -from formula10.database.model.user import User -from formula10.database.model.race import Race -from formula10.database.model.driver import Driver -from formula10 import db - -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" - __csv_header__ = ["user_name", "race_name", "pxx_driver_name", "dnf_driver_name"] - - def __init__(self, user_name: str, race_name: str): - self.user_name = user_name # Primary key - self.race_name = race_name # Primery key - - @staticmethod - def from_csv(row: List[str]): - race_guess: RaceGuess = RaceGuess(str(row[0]), str(row[1])) - race_guess.pxx_driver_name = str(row[2]) - race_guess.dnf_driver_name = str(row[3]) - return race_guess - - def to_csv(self) -> List[Any]: - return [ - self.user_name, - self.race_name, - self.pxx_driver_name, - self.dnf_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"), nullable=True) - dnf_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name"), nullable=True) - - # Relationships - 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]) \ No newline at end of file diff --git a/formula10/database/model/race_result.py b/formula10/database/model/race_result.py deleted file mode 100644 index cd84ed7..0000000 --- a/formula10/database/model/race_result.py +++ /dev/null @@ -1,171 +0,0 @@ -import json -from typing import Any, Dict, List -from sqlalchemy import ForeignKey, String -from sqlalchemy.orm import Mapped, mapped_column, relationship - -from formula10.database.model.driver import Driver -from formula10.database.model.race import Race -from formula10 import db - -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" - __allow_unmapped__ = True # TODO: Used for json conversion, move this to some other class instead - __csv_header__ = ["race_name", "pxx_driver_names_json", "first_dnf_driver_names_json", "dnf_driver_names_json", "excluded_driver_names_json"] - - def __init__(self, race_name: str): - self.race_name = race_name # Primary key - - @staticmethod - def from_csv(row: List[str]): - race_result: RaceResult = RaceResult(str(row[0])) - race_result.pxx_driver_names_json = str(row[1]) - race_result.first_dnf_driver_names_json = str(row[2]) - race_result.dnf_driver_names_json = str(row[3]) - race_result.excluded_driver_names_json = str(row[4]) - return race_result - - def to_csv(self) -> List[Any]: - return [ - self.race_name, - self.pxx_driver_names_json, - self.first_dnf_driver_names_json, - self.dnf_driver_names_json, - self.excluded_driver_names_json - ] - - race_name: Mapped[str] = mapped_column(ForeignKey("race.name"), primary_key=True) - pxx_driver_names_json: Mapped[str] = mapped_column(String(1024), nullable=True) - first_dnf_driver_names_json: Mapped[str] = mapped_column(String(1024), nullable=True) - dnf_driver_names_json: Mapped[str] = mapped_column(String(1024), nullable=True) - excluded_driver_names_json: Mapped[str] = mapped_column(String(1024), nullable=True) - - @property - def pxx_driver_names(self) -> Dict[str, str]: - return json.loads(self.pxx_driver_names_json) - - @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 first_dnf_driver_names(self) -> List[str]: - return json.loads(self.first_dnf_driver_names_json) - - @first_dnf_driver_names.setter - def first_dnf_driver_names(self, new_first_dnf_driver_names: List[str]): - self.first_dnf_driver_names_json = json.dumps(new_first_dnf_driver_names) - - @property - def dnf_driver_names(self) -> List[str]: - return json.loads(self.dnf_driver_names_json) - - @dnf_driver_names.setter - def dnf_driver_names(self, new_dnf_driver_names: List[str]): - self.dnf_driver_names_json = json.dumps(new_dnf_driver_names) - - @property - def excluded_driver_names(self) -> List[str]: - return json.loads(self.excluded_driver_names_json) - - @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_name]) - _pxx_drivers: Dict[str, Driver] | None = None - _first_dnf_drivers: List[Driver] | None = None - _dnf_drivers: List[Driver] | None = None - _excluded_drivers: List[Driver] | None = None - - @property - 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 | None = db.session.query(Driver).filter_by(name=driver_name).first() - if driver is None: - raise Exception(f"Error: Couldn't find driver with id {driver_name}") - - self._pxx_drivers[position] = driver - - return self._pxx_drivers - - def pxx_driver(self, offset: int = 0) -> Driver | None: - pxx_num: str = str(self.race.pxx + offset) - - if pxx_num not in self.pxx_drivers: - raise Exception(f"Position {pxx_num} not found in RaceResult.pxx_drivers") - - if self.pxx_drivers[pxx_num].name in self.excluded_driver_names: - none_driver: Driver | None = db.session.query(Driver).filter_by(name="None").first() - if none_driver is None: - raise Exception(f"NONE-driver not found in database") - - return none_driver - - - return self.pxx_drivers[pxx_num] - - def pxx_driver_position_string(self, driver_name: str) -> str: - for position, driver in self.pxx_driver_names.items(): - if driver == driver_name and driver not in self.excluded_driver_names: - return f"P{position}" - - return "NC" - - @property - def all_positions(self) -> List[Driver]: - return [ - self.pxx_drivers[str(position)] for position in range(1, 21) - ] - - @property - def first_dnf_drivers(self) -> List[Driver]: - if self._first_dnf_drivers is None: - self._first_dnf_drivers = list() - for driver_name in self.first_dnf_driver_names: - driver: Driver | None = db.session.query(Driver).filter_by(name=driver_name).first() - if driver is None: - raise Exception(f"Error: Couldn't find driver with id {driver_name}") - - self._first_dnf_drivers.append(driver) - - if len(self._first_dnf_drivers) == 0: - none_driver: Driver | None = db.session.query(Driver).filter_by(name="None").first() - if none_driver is None: - raise Exception("NONE-driver not found in database") - - self._first_dnf_drivers.append(none_driver) - - return self._first_dnf_drivers - - @property - def dnf_drivers(self) -> List[Driver]: - if self._dnf_drivers is None: - self._dnf_drivers = list() - for driver_name in self.dnf_driver_names: - driver: Driver | None = db.session.query(Driver).filter_by(name=driver_name).first() - if driver is None: - raise Exception(f"Error: Couldn't find driver with id {driver_name}") - - self._dnf_drivers.append(driver) - - return self._dnf_drivers - - @property - def excluded_drivers(self) -> List[Driver]: - if self._excluded_drivers is None: - self._excluded_drivers = list() - for driver_name in self.excluded_driver_names: - driver: Driver | None = db.session.query(Driver).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.append(driver) - - return self._excluded_drivers \ No newline at end of file diff --git a/formula10/database/model/season_guess.py b/formula10/database/model/season_guess.py deleted file mode 100644 index 5319936..0000000 --- a/formula10/database/model/season_guess.py +++ /dev/null @@ -1,81 +0,0 @@ -from typing import Any, List -from sqlalchemy import ForeignKey, String -from sqlalchemy.orm import Mapped, mapped_column, relationship - -from formula10.database.model.driver import Driver -from formula10.database.model.podium_drivers import PodiumDrivers -from formula10.database.model.team import Team -from formula10.database.model.team_winners import TeamWinners -from formula10.database.model.user import User -from formula10 import db - -class SeasonGuess(db.Model): - """ - A collection of bonus guesses for the entire season. - """ - __tablename__ = "seasonguess" - __csv_header__ = ["user_name", "hot_take", "p2_team_name", - "overtake_driver_name", "dnf_driver_name", "gained_driver_name", "lost_driver_name", - "team_winners_id", "podium_drivers_id"] - - def __init__(self, user_name: str, team_winners_user_name: str | None = None, podium_drivers_user_name: str | None = None): - self.user_name = user_name # Primary key - - # Although this is the same username, handle separately, in case they don't exist in the database yet - if team_winners_user_name is not None: - if user_name != team_winners_user_name: - raise Exception(f"SeasonGuess for {user_name} was supplied TeamWinners for {team_winners_user_name}") - - self.team_winners_id = team_winners_user_name - - if podium_drivers_user_name is not None: - if user_name != podium_drivers_user_name: - raise Exception(f"SeasonGuess for {user_name} was supplied PodiumDrivers for {podium_drivers_user_name}") - - self.podium_drivers_id = podium_drivers_user_name - - @staticmethod - def from_csv(row: List[str]): - season_guess: SeasonGuess = SeasonGuess(str(row[0]), team_winners_user_name=str(row[7]), podium_drivers_user_name=str(row[8])) - season_guess.hot_take = str(row[1]) - season_guess.p2_team_name = str(row[2]) - season_guess.overtake_driver_name = str(row[3]) - season_guess.dnf_driver_name = str(row[4]) - season_guess.gained_driver_name = str(row[5]) - season_guess.lost_driver_name = str(row[6]) - return season_guess - - def to_csv(self) -> List[Any]: - return [ - self.user_name, - self.hot_take, - self.p2_team_name, - self.overtake_driver_name, - self.dnf_driver_name, - self.gained_driver_name, - self.lost_driver_name, - self.team_winners_id, - self.podium_drivers_id - ] - - user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True) - hot_take: Mapped[str] = mapped_column(String(512), nullable=True) - p2_team_name: Mapped[str] = mapped_column(ForeignKey("team.name"), nullable=True) - overtake_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name"), nullable=True) - dnf_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name"), nullable=True) - gained_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name"), nullable=True) - lost_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name"), nullable=True) - - team_winners_id: Mapped[str] = mapped_column(ForeignKey("teamwinners.user_name")) - podium_drivers_id: Mapped[str] = mapped_column(ForeignKey("podiumdrivers.user_name")) - - # Relationships - 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=[team_winners_id]) - podium_drivers: Mapped["PodiumDrivers"] = relationship("PodiumDrivers", foreign_keys=[podium_drivers_id]) \ No newline at end of file diff --git a/formula10/database/model/team_winners.py b/formula10/database/model/team_winners.py deleted file mode 100644 index f154918..0000000 --- a/formula10/database/model/team_winners.py +++ /dev/null @@ -1,59 +0,0 @@ -import json -from typing import Any, List -from sqlalchemy import ForeignKey, String -from sqlalchemy.orm import Mapped, mapped_column, relationship - -from formula10.database.model.driver import Driver -from formula10.database.model.user import User -from formula10 import db - -class TeamWinners(db.Model): - """ - A guessed list of each best driver per team. - """ - __tablename__ = "teamwinners" - __allow_unmapped__ = True - __csv_header__ = ["user_name", "teamwinner_driver_names_json"] - - def __init__(self, user_name: str): - self.user_name = user_name # Primary key - - @staticmethod - def from_csv(row: List[str]): - team_winners: TeamWinners = TeamWinners(str(row[0])) - team_winners.teamwinner_driver_names_json = str(row[1]) - return team_winners - - def to_csv(self) -> List[Any]: - return [ - self.user_name, - self.teamwinner_driver_names_json - ] - - user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True) - teamwinner_driver_names_json: Mapped[str] = mapped_column(String(1024), nullable=True) - - @property - def teamwinner_driver_names(self) -> List[str]: - return json.loads(self.teamwinner_driver_names_json) - - @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_name]) - _teamwinner_drivers: List[Driver] | None = None - - @property - def teamwinners(self) -> List[Driver]: - if self._teamwinner_drivers is None: - self._teamwinner_drivers = list() - for driver_name in self.teamwinner_driver_names: - driver: Driver | None = db.session.query(Driver).filter_by(name=driver_name).first() - if driver is None: - raise Exception(f"Error: Couldn't find driver with id {driver_name}") - - self._teamwinner_drivers.append(driver) - - return self._teamwinner_drivers \ No newline at end of file diff --git a/formula10/database/model/user.py b/formula10/database/model/user.py deleted file mode 100644 index 4173332..0000000 --- a/formula10/database/model/user.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Any, List -from urllib.parse import quote -from sqlalchemy import String -from sqlalchemy.orm import Mapped, mapped_column - -from formula10 import db - -class User(db.Model): - """ - A user that can guess races (name only). - """ - __tablename__ = "user" - __csv_header__ = ["name"] - - def __init__(self, name: str): - self.name = name # Primary key - - @staticmethod - def from_csv(row: List[str]): - user: User = User(str(row[0])) - return user - - def to_csv(self) -> List[Any]: - return [ - self.name - ] - - @property - def name_sanitized(self) -> str: - return quote(self.name) - - name: Mapped[str] = mapped_column(String(32), primary_key=True) \ No newline at end of file diff --git a/formula10/database/update_query_util.py b/formula10/database/update_queries.py similarity index 51% rename from formula10/database/update_query_util.py rename to formula10/database/update_queries.py index 5a54802..9a04ce4 100644 --- a/formula10/database/update_query_util.py +++ b/formula10/database/update_queries.py @@ -1,32 +1,31 @@ +import json from typing import Dict, List, cast from urllib.parse import quote from flask import redirect from werkzeug import Response -from formula10.database.common_query_util import race_has_result, user_exists -from formula10.database.model.podium_drivers import PodiumDrivers -from formula10.database.model.race_guess import RaceGuess -from formula10.database.model.race_result import RaceResult -from formula10.database.model.season_guess import SeasonGuess -from formula10.database.model.team_winners import TeamWinners -from formula10.database.model.user import User -from formula10.database.validation_util import any_is_none, positions_are_contiguous +from formula10.database.common_queries import race_has_result, user_exists_and_disabled, user_exists_and_enabled +from formula10.database.model.db_race_guess import DbRaceGuess +from formula10.database.model.db_race_result import DbRaceResult +from formula10.database.model.db_season_guess import DbSeasonGuess +from formula10.database.model.db_user import DbUser +from formula10.database.validation import any_is_none, positions_are_contiguous from formula10 import db -def find_or_create_race_guess(user_name: str, race_name: str) -> RaceGuess: +def find_or_create_race_guess(user_name: str, race_name: str) -> DbRaceGuess: # There can be a single RaceGuess at most, since (user_name, race_name) is the composite primary key - race_guess: RaceGuess | None = db.session.query(RaceGuess).filter_by(user_name=user_name, race_name=race_name).first() + race_guess: DbRaceGuess | None = db.session.query(DbRaceGuess).filter_by(user_name=user_name, race_name=race_name).first() if race_guess is not None: return race_guess # Insert a new RaceGuess - race_guess = RaceGuess(user_name=user_name, race_name=race_name) + race_guess = DbRaceGuess(user_name=user_name, race_name=race_name, pxx_driver_name="TEMP", dnf_driver_name="TEMP") db.session.add(race_guess) db.session.commit() # Double check if database insertion worked and obtain any values set by the database - race_guess = db.session.query(RaceGuess).filter_by(user_name=user_name, race_name=race_name).first() + race_guess = db.session.query(DbRaceGuess).filter_by(user_name=user_name, race_name=race_name).first() if race_guess is None: raise Exception("Failed adding RaceGuess to the database") @@ -46,7 +45,7 @@ def update_race_guess(race_name: str, user_name: str, pxx_select: str | None, dn if race_has_result(race_name): return redirect(f"/race/{quote(user_name)}") - race_guess: RaceGuess = find_or_create_race_guess(user_name, race_name) + race_guess: DbRaceGuess = find_or_create_race_guess(user_name, race_name) race_guess.pxx_driver_name = pxx_driver_name race_guess.dnf_driver_name = dnf_driver_name @@ -60,70 +59,25 @@ def delete_race_guess(race_name: str, user_name: str) -> Response: if race_has_result(race_name): return redirect(f"/race/{quote(user_name)}") - db.session.query(RaceGuess).filter_by(race_name=race_name, user_name=user_name).delete() + db.session.query(DbRaceGuess).filter_by(race_name=race_name, user_name=user_name).delete() db.session.commit() return redirect("/race/Everyone") -def find_or_create_team_winners(user_name: str) -> TeamWinners: - # There can be a single TeamWinners at most, since user_name is the primary key - team_winners: TeamWinners | None = db.session.query(TeamWinners).filter_by(user_name=user_name).first() - if team_winners is not None: - return team_winners - - team_winners = TeamWinners(user_name=user_name) - db.session.add(team_winners) - db.session.commit() - - # Double check if database insertion worked and obtain any values set by the database - team_winners = db.session.query(TeamWinners).filter_by(user_name=user_name).first() - if team_winners is None: - raise Exception("Failed adding TeamWinners to the database") - - return team_winners - - -def find_or_create_podium_drivers(user_name: str) -> PodiumDrivers: - # There can be a single PodiumDrivers at most, since user_name is the primary key - podium_drivers: PodiumDrivers | None = db.session.query(PodiumDrivers).filter_by(user_name=user_name).first() - if podium_drivers is not None: - return podium_drivers - - podium_drivers = PodiumDrivers(user_name=user_name) - db.session.add(podium_drivers) - db.session.commit() - - # Double check if database insertion worked and obtain any values set by the database - podium_drivers = db.session.query(PodiumDrivers).filter_by(user_name=user_name).first() - if podium_drivers is None: - raise Exception("Failed adding PodiumDrivers to the database") - - return podium_drivers - - -def find_or_create_season_guess(user_name: str) -> SeasonGuess: +def find_or_create_season_guess(user_name: str) -> DbSeasonGuess: # There can be a single SeasonGuess at most, since user_name is the primary key - season_guess: SeasonGuess | None = db.session.query(SeasonGuess).filter_by(user_name=user_name).first() + season_guess: DbSeasonGuess | None = db.session.query(DbSeasonGuess).filter_by(user_name=user_name).first() if season_guess is not None: - # There can't be more than a single one, since both also use user_name as primary key - if db.session.query(TeamWinners).filter_by(user_name=user_name).first() is None: - raise Exception(f"SeasonGuess for {user_name} is missing associated TeamWinners") - if db.session.query(PodiumDrivers).filter_by(user_name=user_name).first() is None: - raise Exception(f"SeasonGuess for {user_name} is missing associated PodiumDrivers") - return season_guess # Insert a new SeasonGuess - team_winners: TeamWinners = find_or_create_team_winners(user_name) - podium_drivers: PodiumDrivers = find_or_create_podium_drivers(user_name) - - season_guess = SeasonGuess(user_name=user_name, team_winners_user_name=team_winners.user_name, podium_drivers_user_name=podium_drivers.user_name) + season_guess = DbSeasonGuess(user_name=user_name, team_winners_driver_names_json=json.dumps(["TEMP"]), podium_drivers_driver_names_json=json.dumps(["TEMP"])) db.session.add(season_guess) db.session.commit() # Double check if database insertion worked and obtain any values set by the database - season_guess = db.session.query(SeasonGuess).filter_by(user_name=user_name).first() + season_guess = db.session.query(DbSeasonGuess).filter_by(user_name=user_name).first() if season_guess is None: raise Exception("Failed adding SeasonGuess to the database") @@ -133,33 +87,37 @@ def find_or_create_season_guess(user_name: str) -> SeasonGuess: def update_season_guess(user_name: str, guesses: List[str | None], team_winner_guesses: List[str | None], podium_driver_guesses: List[str]) -> Response: # Pylance marks type errors here, but those are intended. Columns are marked nullable. - season_guess: SeasonGuess = find_or_create_season_guess(user_name) + season_guess: DbSeasonGuess = find_or_create_season_guess(user_name) season_guess.hot_take = guesses[0] # type: ignore season_guess.p2_team_name = guesses[1] # type: ignore season_guess.overtake_driver_name = guesses[2] # type: ignore season_guess.dnf_driver_name = guesses[3] # type: ignore season_guess.gained_driver_name = guesses[4] # type: ignore season_guess.lost_driver_name = guesses[5] # type: ignore - season_guess.team_winners.teamwinner_driver_names = team_winner_guesses # type: ignore - season_guess.podium_drivers.podium_driver_names = podium_driver_guesses + season_guess.team_winners_driver_names_json = json.dumps(team_winner_guesses) + season_guess.podium_drivers_driver_names_json = json.dumps(podium_driver_guesses) db.session.commit() return redirect(f"/season/Everyone") -def find_or_create_race_result(race_name: str) -> RaceResult: +def find_or_create_race_result(race_name: str) -> DbRaceResult: # There can be a single RaceResult at most, since race_name is the primary key - race_result: RaceResult | None = db.session.query(RaceResult).filter_by(race_name=race_name).first() + race_result: DbRaceResult | None = db.session.query(DbRaceResult).filter_by(race_name=race_name).first() if race_result is not None: return race_result - race_result = RaceResult(race_name=race_name) + race_result = DbRaceResult(race_name=race_name, + pxx_driver_names_json=json.dumps(["TEMP"]), + first_dnf_driver_names_json=json.dumps(["TEMP"]), + dnf_driver_names_json=json.dumps(["TEMP"]), + excluded_driver_names_json=json.dumps(["TEMP"])) db.session.add(race_result) db.session.commit() # Double check if database insertion worked and obtain any values set by the database - race_result = db.session.query(RaceResult).filter_by(race_name=race_name).first() + race_result = db.session.query(DbRaceResult).filter_by(race_name=race_name).first() if race_result is None: raise Exception("Failed adding RaceResult to the database") @@ -185,11 +143,15 @@ def update_race_result(race_name: str, pxx_driver_names_list: List[str], first_d if driver_name not in dnf_driver_names_list: dnf_driver_names_list.append(driver_name) - race_result: RaceResult = find_or_create_race_result(race_name) - race_result.pxx_driver_names = pxx_driver_names - race_result.first_dnf_driver_names = first_dnf_driver_names_list - race_result.dnf_driver_names = dnf_driver_names_list - race_result.excluded_driver_names = excluded_driver_names_list + # There can't be dnfs but no initial dnfs + if len(dnf_driver_names_list) > 0 and len(first_dnf_driver_names_list) == 0: + return redirect(f"/result/{quote(race_name)}") + + race_result: DbRaceResult = find_or_create_race_result(race_name) + race_result.pxx_driver_names_json = json.dumps(pxx_driver_names) + race_result.first_dnf_driver_names_json = json.dumps(first_dnf_driver_names_list) + race_result.dnf_driver_names_json = json.dumps(dnf_driver_names_list) + race_result.excluded_driver_names_json = json.dumps(excluded_driver_names_list) db.session.commit() @@ -207,21 +169,32 @@ def update_user(user_name: str | None, add: bool = False, delete: bool = False) return redirect("/user") if add: - if user_exists(user_name): + if user_exists_and_enabled(user_name): return redirect("/user") - user: User = User(name=user_name) - db.session.add(user) + elif user_exists_and_disabled(user_name): + disabled_user: DbUser | None = db.session.query(DbUser).filter_by(name=user_name, enabled=False).first() + if disabled_user is None: + raise Exception("update_user couldn't reenable user") + + disabled_user.enabled = True + + else: + user: DbUser = DbUser(name=user_name, enabled=True) + db.session.add(user) + db.session.commit() return redirect("/user") if delete: - if not user_exists(user_name): - return redirect("/user") + if user_exists_and_enabled(user_name): + enabled_user: DbUser | None = db.session.query(DbUser).filter_by(name=user_name, enabled=True).first() + if enabled_user is None: + raise Exception("update_user couldn't disable user") - db.session.query(User).filter_by(name=user_name).delete() - db.session.commit() + enabled_user.enabled = False + db.session.commit() return redirect("/user") diff --git a/formula10/database/validation_util.py b/formula10/database/validation.py similarity index 79% rename from formula10/database/validation_util.py rename to formula10/database/validation.py index b8e231d..78fab87 100644 --- a/formula10/database/validation_util.py +++ b/formula10/database/validation.py @@ -22,7 +22,7 @@ def positions_are_contiguous(positions: List[str]) -> bool: return positions_sorted[0] + len(positions_sorted) - 1 == positions_sorted[-1] -def find_first_or_none(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T | None: +def find_first_else_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. @@ -30,7 +30,13 @@ def find_first_or_none(predicate: Callable[[_T], bool], iterable: Iterable[_T]) return next(filter(predicate, iterable), None) -def find_multiple(predicate: Callable[[_T], bool], iterable: Iterable[_T], count: int = 0) -> List[_T]: +def find_multiple(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> List[_T]: + filtered = list(filter(predicate, iterable)) + + return filtered + + +def find_multiple_strict(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. @@ -43,7 +49,7 @@ def find_multiple(predicate: Callable[[_T], bool], iterable: Iterable[_T], count return filtered -def find_single(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T: +def find_single_strict(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. @@ -56,7 +62,7 @@ def find_single(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T: return filtered[0] -def find_single_or_none(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T | None: +def find_single_or_none_strict(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. diff --git a/formula10/frontend/model/driver.py b/formula10/frontend/model/driver.py new file mode 100644 index 0000000..b1df1dd --- /dev/null +++ b/formula10/frontend/model/driver.py @@ -0,0 +1,44 @@ +from urllib.parse import quote + +from formula10.database.model.db_driver import DbDriver +from formula10.frontend.model.team import NONE_TEAM, Team + + +class Driver(): + @classmethod + def from_db_driver(cls, db_driver: DbDriver): + driver: Driver = cls() + driver.name = db_driver.name + driver.abbr = db_driver.abbr + driver.country = db_driver.country_code + driver.team = Team.from_db_team(db_driver.team) + return driver + + def to_db_driver(self) -> DbDriver: + db_driver: DbDriver = DbDriver(name=self.name) + db_driver.abbr = self.abbr + db_driver.country_code = self.country + db_driver.team_name = self.team.name + return db_driver + + def __eq__(self, __value: object) -> bool: + if isinstance(__value, Driver): + return self.name == __value.name + + return NotImplemented + + name: str + abbr: str + country: str + team: Team + + @property + def name_sanitized(self) -> str: + return quote(self.name) + + +NONE_DRIVER: Driver = Driver() +NONE_DRIVER.name = "None" +NONE_DRIVER.abbr = "None" +NONE_DRIVER.country = "NO" +NONE_DRIVER.team = NONE_TEAM \ No newline at end of file diff --git a/formula10/frontend/model/race.py b/formula10/frontend/model/race.py new file mode 100644 index 0000000..1e36cc6 --- /dev/null +++ b/formula10/frontend/model/race.py @@ -0,0 +1,37 @@ +from datetime import datetime +from urllib.parse import quote + +from formula10.database.model.db_race import DbRace + + +class Race(): + @classmethod + def from_db_race(cls, db_race: DbRace): + race: Race = cls() + race.name = db_race.name + race.number = db_race.number + race.date = db_race.date + race.place_to_guess = db_race.pxx + return race + + def to_db_race(self) -> DbRace: + db_race: DbRace = DbRace(name=self.name, + number=self.number, + date=self.date, + pxx=self.place_to_guess) + return db_race + + def __eq__(self, __value: object) -> bool: + if isinstance(__value, Race): + return self.name == __value.name + + return NotImplemented + + name: str + number: int + date: datetime + place_to_guess: int + + @property + def name_sanitized(self) -> str: + return quote(self.name) \ No newline at end of file diff --git a/formula10/frontend/model/race_guess.py b/formula10/frontend/model/race_guess.py new file mode 100644 index 0000000..4d1cd29 --- /dev/null +++ b/formula10/frontend/model/race_guess.py @@ -0,0 +1,33 @@ +from formula10.database.model.db_race_guess import DbRaceGuess +from formula10.frontend.model.driver import Driver +from formula10.frontend.model.race import Race +from formula10.frontend.model.user import User + + +class RaceGuess(): + @classmethod + def from_db_race_guess(cls, db_race_guess: DbRaceGuess): + race_guess: RaceGuess = cls() + race_guess.user = User.from_db_user(db_race_guess.user) + race_guess.race = Race.from_db_race(db_race_guess.race) + race_guess.pxx_guess = Driver.from_db_driver(db_race_guess.pxx) + race_guess.dnf_guess = Driver.from_db_driver(db_race_guess.dnf) + return race_guess + + def to_db_race_guess(self) -> DbRaceGuess: + db_race_guess: DbRaceGuess = DbRaceGuess(user_name=self.user.name, + race_name=self.race.name, + pxx_driver_name=self.pxx_guess.name, + dnf_driver_name=self.dnf_guess.name) + return db_race_guess + + def __eq__(self, __value: object) -> bool: + if isinstance(__value, RaceGuess): + return self.user == __value.user and self.race == __value.race + + return NotImplemented + + user: User + race: Race + pxx_guess: Driver + dnf_guess: Driver diff --git a/formula10/frontend/model/race_result.py b/formula10/frontend/model/race_result.py new file mode 100644 index 0000000..41bc9d2 --- /dev/null +++ b/formula10/frontend/model/race_result.py @@ -0,0 +1,148 @@ +import json +from typing import Dict, List + +from formula10.database.common_queries import find_single_driver_strict +from formula10.database.model.db_race_result import DbRaceResult +from formula10.frontend.model.driver import NONE_DRIVER, Driver +from formula10.frontend.model.race import Race + + +class RaceResult: + @classmethod + def from_db_race_result(cls, db_race_result: DbRaceResult): + race_result: RaceResult = cls() + race_result.race = Race.from_db_race(db_race_result.race) + + # Deserialize from json + standing: Dict[str, str] = json.loads(db_race_result.pxx_driver_names_json) + initial_dnf: List[str] = json.loads(db_race_result.first_dnf_driver_names_json) + all_dnfs: List[str] = json.loads(db_race_result.dnf_driver_names_json) + standing_exclusions: List[str] = json.loads(db_race_result.excluded_driver_names_json) + + # Populate relationships + race_result.standing = { + position: Driver.from_db_driver(find_single_driver_strict(driver_name)) + for position, driver_name in standing.items() + } + race_result.initial_dnf = [ + Driver.from_db_driver(find_single_driver_strict(driver_name)) + for driver_name in initial_dnf + ] + race_result.all_dnfs = [ + Driver.from_db_driver(find_single_driver_strict(driver_name)) + for driver_name in all_dnfs + ] + race_result.standing_exclusions = [ + Driver.from_db_driver(find_single_driver_strict(driver_name)) + for driver_name in standing_exclusions + ] + + return race_result + + def to_db_race_result(self) -> DbRaceResult: + # "Unpopulate" relationships, remove none driver + standing: Dict[str, str] = { + position: driver.name for position, driver in self.standing.items() + } + initial_dnf: List[str] = [ + driver.name for driver in self.initial_dnf if driver + ] + all_dnfs: List[str] = [ + driver.name for driver in self.all_dnfs if driver + ] + standing_exclusions: List[str] = [ + driver.name for driver in self.standing_exclusions if driver + ] + + # Serialize to json + db_race_result: DbRaceResult = DbRaceResult(race_name=self.race.name, + pxx_driver_names_json=json.dumps(standing), + first_dnf_driver_names_json=json.dumps(initial_dnf), + dnf_driver_names_json=json.dumps(all_dnfs), + excluded_driver_names_json=json.dumps(standing_exclusions)) + + return db_race_result + + def __eq__(self, __value: object) -> bool: + if isinstance(__value, RaceResult): + return self.race == __value.race + + return NotImplemented + + race: Race + standing: Dict[str, Driver] + initial_dnf: List[Driver] + all_dnfs: List[Driver] + standing_exclusions: List[Driver] + + def offset_from_place_to_guess(self, offset: int, respect_nc:bool = True) -> Driver: + position: str = str(self.race.place_to_guess + offset) + + if position not in self.standing: + raise Exception(f"Position {position} not found in RaceResult.standing") + + if self.standing[position] in self.standing_exclusions and respect_nc: + return NONE_DRIVER + + return self.standing[position] + + def driver_standing_position_string(self, driver: Driver) -> str: + if driver == NONE_DRIVER: + return "" + + for position, _driver in self.standing.items(): + if driver == _driver and driver not in self.standing_exclusions: + return f" (P{position})" + + return " (NC)" + + def driver_standing_points_string(self, driver: Driver) -> str: + points_strings: Dict[int, str] = { + 0: "10 Points", + 1: "6 Points", + 2: "3 Points", + 3: "1 Points" + } + + if driver == NONE_DRIVER: + if self.standing[str(self.race.place_to_guess)] in self.standing_exclusions: + return "10 Points" + else: + return "0 Points" + + for position, _driver in self.standing.items(): + if driver == _driver and driver not in self.standing_exclusions: + position_offset: int = abs(self.race.place_to_guess - int(position)) + if position_offset in points_strings: + return points_strings[position_offset] + else: + return "0 Points" + + raise Exception(f"Could not get points string for driver {driver.name}") + + def driver_dnf_points_string(self, driver: Driver) -> str: + if driver == NONE_DRIVER: + if len(self.initial_dnf) == 0: + return "10 Points" + else: + return "0 Points" + + if driver in self.initial_dnf: + return "10 Points" + else: + return "0 Points" + + def ordered_standing_list(self) -> List[Driver]: + return [ + self.standing[str(position)] for position in range(1, 21) + ] + + def initial_dnf_string(self) -> str: + if len(self.initial_dnf) == 0: + return NONE_DRIVER.name + + dnf_string: str = "" + for driver in self.initial_dnf: + dnf_string += f"{driver.abbr} " + + return dnf_string[0:len(dnf_string)-1] # Remove last space diff --git a/formula10/frontend/model/season_guess.py b/formula10/frontend/model/season_guess.py new file mode 100644 index 0000000..6ecd4d9 --- /dev/null +++ b/formula10/frontend/model/season_guess.py @@ -0,0 +1,73 @@ +import json +from typing import List +from formula10.database.common_queries import find_single_driver_strict +from formula10.database.model.db_season_guess import DbSeasonGuess +from formula10.frontend.model.driver import Driver +from formula10.frontend.model.team import Team +from formula10.frontend.model.user import User + + +class SeasonGuess(): + @classmethod + def from_db_season_guess(cls, db_season_guess: DbSeasonGuess): + season_guess: SeasonGuess = cls() + season_guess.user = User.from_db_user(db_season_guess.user) + season_guess.hot_take = db_season_guess.hot_take if db_season_guess.hot_take is not None else None + season_guess.p2_wcc = Team.from_db_team(db_season_guess.p2_team) if db_season_guess.p2_team is not None else None + season_guess.most_overtakes = Driver.from_db_driver(db_season_guess.overtake_driver) if db_season_guess.overtake_driver is not None else None + season_guess.most_dnfs = Driver.from_db_driver(db_season_guess.dnf_driver) if db_season_guess.dnf_driver is not None else None + season_guess.most_wdc_gained = Driver.from_db_driver(db_season_guess.gained_driver) if db_season_guess.gained_driver is not None else None + season_guess.most_wdc_lost = Driver.from_db_driver(db_season_guess.lost_driver) if db_season_guess.lost_driver is not None else None + + # Deserialize from json + team_winners: List[str | None] = json.loads(db_season_guess.team_winners_driver_names_json) + podiums: List[str] = json.loads(db_season_guess.podium_drivers_driver_names_json) + + # Populate relationships + season_guess.team_winners = [ + Driver.from_db_driver(find_single_driver_strict(driver_name)) if driver_name is not None else None + for driver_name in team_winners + ] + season_guess.podiums = [ + Driver.from_db_driver(find_single_driver_strict(driver_name)) + for driver_name in podiums + ] + + return season_guess + + def to_db_season_guess(self): + # "Unpopulate" relationships + team_winners: List[str | None] = [ + driver.name if driver is not None else None + for driver in self.team_winners + ] + podiums: List[str] = [ + driver.name for driver in self.podiums + ] + + # Serialize to json + db_season_guess: DbSeasonGuess = DbSeasonGuess(user_name=self.user.name, + team_winners_driver_names_json=json.dumps(team_winners), + podium_drivers_driver_names_json=json.dumps(podiums)) + db_season_guess.user_name = self.user.name + db_season_guess.hot_take = self.hot_take + db_season_guess.p2_team_name = self.p2_wcc.name if self.p2_wcc is not None else None + db_season_guess.overtake_driver_name = self.most_overtakes.name if self.most_overtakes is not None else None + db_season_guess.dnf_driver_name = self.most_dnfs.name if self.most_dnfs is not None else None + db_season_guess.gained_driver_name = self.most_wdc_gained.name if self.most_wdc_gained is not None else None + db_season_guess.lost_driver_name = self.most_wdc_lost.name if self.most_wdc_lost is not None else None + + return db_season_guess + + user: User + hot_take: str | None + p2_wcc: Team | None + most_overtakes: Driver | None + most_dnfs: Driver | None + most_wdc_gained: Driver | None + most_wdc_lost: Driver | None + team_winners: List[Driver | None] + podiums: List[Driver] + + def hot_take_string(self) -> str: + return self.hot_take if self.hot_take is not None else "" diff --git a/formula10/frontend/model/team.py b/formula10/frontend/model/team.py new file mode 100644 index 0000000..51925fc --- /dev/null +++ b/formula10/frontend/model/team.py @@ -0,0 +1,30 @@ +from urllib.parse import quote + +from formula10.database.model.db_team import DbTeam + + +class Team(): + @classmethod + def from_db_team(cls, db_team: DbTeam): + team: Team = cls() + team.name = db_team.name + return team + + def to_db_team(self) -> DbTeam: + db_team: DbTeam = DbTeam(name=self.name) + return db_team + + def __eq__(self, __value: object) -> bool: + if isinstance(__value, Team): + return self.name == __value.name + + return NotImplemented + + name: str + + @property + def name_sanitized(self) -> str: + return quote(self.name) + +NONE_TEAM: Team = Team() +NONE_TEAM.name = "None" \ No newline at end of file diff --git a/formula10/frontend/model/user.py b/formula10/frontend/model/user.py new file mode 100644 index 0000000..7dc18b6 --- /dev/null +++ b/formula10/frontend/model/user.py @@ -0,0 +1,29 @@ +from urllib.parse import quote + +from formula10.database.model.db_user import DbUser + + +class User(): + @classmethod + def from_db_user(cls, db_user: DbUser): + user: User = cls() + user.name = db_user.name + user.enabled = db_user.enabled + return user + + def to_db_user(self) -> DbUser: + db_user: DbUser = DbUser(name=self.name, enabled=self.enabled) + return db_user + + def __eq__(self, __value: object) -> bool: + if isinstance(__value, User): + return self.name == __value.name + + return NotImplemented + + name: str + enabled: bool + + @property + def name_sanitized(self) -> str: + return quote(self.name) diff --git a/formula10/frontend/template_model.py b/formula10/frontend/template_model.py index 4a59a0a..53db508 100644 --- a/formula10/frontend/template_model.py +++ b/formula10/frontend/template_model.py @@ -1,18 +1,24 @@ from typing import List, Callable, Dict, overload from sqlalchemy import desc -from formula10.database.model.driver import Driver -from formula10.database.model.race import Race -from formula10.database.model.race_guess import RaceGuess -from formula10.database.model.race_result import RaceResult -from formula10.database.model.season_guess import SeasonGuess -from formula10.database.model.team import Team -from formula10.database.model.user import User -from formula10.database.validation_util import find_first_or_none, find_multiple, find_single, find_single_or_none +from formula10.database.model.db_driver import DbDriver +from formula10.database.model.db_race import DbRace +from formula10.database.model.db_race_guess import DbRaceGuess +from formula10.database.model.db_race_result import DbRaceResult +from formula10.database.model.db_season_guess import DbSeasonGuess +from formula10.database.model.db_team import DbTeam +from formula10.database.model.db_user import DbUser +from formula10.frontend.model.driver import NONE_DRIVER, Driver +from formula10.frontend.model.race import Race +from formula10.frontend.model.race_guess import RaceGuess +from formula10.frontend.model.race_result import RaceResult +from formula10.frontend.model.season_guess import SeasonGuess +from formula10.frontend.model.team import NONE_TEAM, Team +from formula10.frontend.model.user import User +from formula10.database.validation import find_first_else_none, find_multiple_strict, find_single_strict, find_single_or_none_strict from formula10 import db -# This could also be moved to database_utils (at least partially), but I though the template should cache the database responses class TemplateModel: """ This class bundles all data required from inside a template. @@ -26,15 +32,42 @@ class TemplateModel: _all_drivers: List[Driver] | None = None _all_teams: List[Team] | None = None + active_user: User | None = None + active_result: RaceResult | None = None + + _wdc_gained_excluded_abbrs: List[str] = ["RIC"] + + def __init__(self, *, active_user_name: str | None, active_result_race_name: str | None): + if active_user_name is not None: + self.active_user = self.user_by(user_name=active_user_name, ignore=["Everyone"]) + + if active_result_race_name is not None: + self.active_result = self.race_result_by(race_name=active_result_race_name) + + def active_user_name_or_everyone(self) -> str: + return self.active_user.name if self.active_user is not None else "Everyone" + + def active_user_name_sanitized_or_everyone(self) -> str: + return self.active_user.name_sanitized if self.active_user is not None else "Everyone" + def all_users(self) -> List[User]: """ Returns a list of all users in the database. """ if self._all_users is None: - self._all_users = db.session.query(User).all() + self._all_users = [ + User.from_db_user(db_user) + for db_user in db.session.query(DbUser).filter_by(enabled=True).all() + ] return self._all_users + def all_users_or_active_user(self) -> List[User]: + if self.active_user is not None: + return [self.active_user] + + return self.all_users() + @overload def user_by(self, *, user_name: str) -> User: """ @@ -57,14 +90,17 @@ class TemplateModel: return None predicate: Callable[[User], bool] = lambda user: user.name == user_name - return find_single(predicate, self.all_users()) + return find_single_strict(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 = db.session.query(RaceResult).join(RaceResult.race).order_by(desc(Race.number)).all() + self._all_race_results = [ + RaceResult.from_db_race_result(db_race_result) + for db_race_result in db.session.query(DbRaceResult).join(DbRaceResult.race).order_by(desc(DbRace.number)).all() + ] return self._all_race_results @@ -73,14 +109,17 @@ class TemplateModel: 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()) + return find_single_or_none_strict(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 = db.session.query(RaceGuess).all() + self._all_race_guesses = [ + RaceGuess.from_db_race_guess(db_race_guess) + for db_race_guess in db.session.query(DbRaceGuess).join(DbRaceGuess.user).filter_by(enabled=True).all() # Ignore disabled users + ] return self._all_race_guesses @@ -115,18 +154,18 @@ class TemplateModel: def race_guesses_by(self, *, user_name: str | None = None, race_name: str | None = 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()) + predicate: Callable[[RaceGuess], bool] = lambda guess: guess.user.name == user_name + return find_multiple_strict(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()) + predicate: Callable[[RaceGuess], bool] = lambda guess: guess.race.name == race_name + return find_multiple_strict(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()) + predicate: Callable[[RaceGuess], bool] = lambda guess: guess.user.name == user_name and guess.race.name == race_name + return find_single_or_none_strict(predicate, self.all_race_guesses()) # Dict with all guesses if user_name is None and race_name is None: @@ -134,10 +173,10 @@ class TemplateModel: guess: RaceGuess for guess in self.all_race_guesses(): - if guess.race_name not in guesses_by: - guesses_by[guess.race_name] = dict() + if guess.race.name not in guesses_by: + guesses_by[guess.race.name] = dict() - guesses_by[guess.race_name][guess.user_name] = guess + guesses_by[guess.race.name][guess.user.name] = guess return guesses_by @@ -145,7 +184,10 @@ class TemplateModel: def all_season_guesses(self) -> List[SeasonGuess]: if self._all_season_guesses is None: - self._all_season_guesses = db.session.query(SeasonGuess).all() + self._all_season_guesses = [ + SeasonGuess.from_db_season_guess(db_season_guess) + for db_season_guess in db.session.query(DbSeasonGuess).join(DbSeasonGuess.user).filter_by(enabled=True).all() # Ignore disabled users + ] return self._all_season_guesses @@ -165,15 +207,15 @@ class TemplateModel: def season_guesses_by(self, *, user_name: str | None = 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()) + predicate: Callable[[SeasonGuess], bool] = lambda guess: guess.user.name == user_name + return find_single_or_none_strict(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 + guesses_by[guess.user.name] = guess return guesses_by @@ -184,7 +226,10 @@ class TemplateModel: Returns a list of all races in the database. """ if self._all_races is None: - self._all_races = db.session.query(Race).order_by(desc(Race.number)).all() + self._all_races = [ + Race.from_db_race(db_race) + for db_race in db.session.query(DbRace).order_by(desc(DbRace.number)).all() + ] return self._all_races @@ -199,32 +244,72 @@ class TemplateModel: 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()) + return find_first_else_none(predicate, self.all_races()) - def all_teams(self) -> List[Team]: + @property + def current_race(self) -> Race | None: + return self.first_race_without_result() + + def active_result_race_name_or_current_race_name(self) -> str: + if self.active_result is not None: + return self.active_result.race.name + elif self.current_race is not None: + return self.current_race.name + else: + return self.all_race_results()[0].race.name + + def active_result_race_name_or_current_race_name_sanitized(self) -> str: + if self.active_result is not None: + return self.active_result.race.name_sanitized + elif self.current_race is not None: + return self.current_race.name_sanitized + else: + return self.all_race_results()[0].race.name_sanitized + + def all_teams(self, *, include_none: bool) -> List[Team]: """ Returns a list of all teams in the database. """ if self._all_teams is None: - self._all_teams = db.session.query(Team).all() + self._all_teams = [ + Team.from_db_team(db_team) + for db_team in db.session.query(DbTeam).all() + ] - return self._all_teams + if include_none: + return self._all_teams + else: + predicate: Callable[[Team], bool] = lambda team: team != NONE_TEAM + return find_multiple_strict(predicate, self._all_teams) - def all_drivers(self) -> List[Driver]: + def none_team(self) -> Team: + return NONE_TEAM + + def all_drivers(self, *, include_none: bool) -> List[Driver]: """ - Returns a list of all drivers in the database, including the NONE driver. + Returns a list of all drivers in the database. """ if self._all_drivers is None: - self._all_drivers = db.session.query(Driver).all() + self._all_drivers = [ + Driver.from_db_driver(db_driver) + for db_driver in db.session.query(DbDriver).all() + ] - return self._all_drivers + if include_none: + return self._all_drivers + else: + predicate: Callable[[Driver], bool] = lambda driver: driver != NONE_DRIVER + return find_multiple_strict(predicate, 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()) + def all_drivers_or_active_result_standing_drivers(self) -> List[Driver]: + return self.active_result.ordered_standing_list() if self.active_result is not None else self.all_drivers(include_none=False) + + def drivers_for_wdc_gained(self) -> List[Driver]: + predicate: Callable[[Driver], bool] = lambda driver: driver.abbr not in self._wdc_gained_excluded_abbrs + return find_multiple_strict(predicate, self.all_drivers(include_none=False)) + + def none_driver(self) -> Driver: + return NONE_DRIVER @overload def drivers_by(self, *, team_name: str) -> List[Driver]: @@ -243,16 +328,16 @@ class TemplateModel: def drivers_by(self, *, team_name: str | None = 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) + return find_multiple_strict(predicate, self.all_drivers(include_none=False), 2) if team_name is None: drivers_by: Dict[str, List[Driver]] = dict() driver: Driver team: Team - for team in self.all_teams(): + for team in self.all_teams(include_none=False): drivers_by[team.name] = [] - for driver in self.all_drivers_except_none(): + for driver in self.all_drivers(include_none=False): drivers_by[driver.team.name] += [driver] return drivers_by diff --git a/formula10/templates/base.jinja b/formula10/templates/base.jinja index 618d7a1..824008c 100644 --- a/formula10/templates/base.jinja +++ b/formula10/templates/base.jinja @@ -1,15 +1,36 @@ -{# Simple driver dropdown. Requires list of drivers. #} -{% macro driver_select(name='', label='', include_none=true) %} +{# Active user navbar dropdown #} +{% macro active_user_dropdown(page) %} + {% if model.all_users() | length > 1 %} + + {% endif %} +{% endmacro %} + +{# Simple driver select for forms #} +{% macro driver_select(name, label, include_none, drivers=none) %}
{# Use namespace wrapper to persist scope between loop iterations #} - {% set user_has_chosen = namespace(driverpre="false") %} + {% set user_has_chosen = namespace(driverpre=false) %} - {% if include_none == true %} - {% set drivers = model.all_drivers() %} - {% else %} - {% set drivers = model.all_drivers_except_none() %} + {% if drivers == none %} + {% set drivers = model.all_drivers(include_none=include_none) %} {% endif %} {% for driver in drivers %} - {% if match == driver.abbr %} - {% set user_has_chosen.driverpre = "true" %} + {% if driver_match == driver %} + {% set user_has_chosen.driverpre = true %} {% else %} {% endif %} - {% if (include_none == true) and (driver.abbr == "None") %} + {% if (include_none == true) and (driver == model.none_driver()) %} {% endif %} {% endfor %} {# Add an empty default if nothing has been chosen #} - {% if user_has_chosen.driverpre == "false" %} + {% if user_has_chosen.driverpre == false %} {% endif %} @@ -55,12 +74,17 @@
{% endmacro %} -{# Simple team dropdown. Requires list of teams. #} -{% macro team_select(name='', label='') %} +{# Simple team select for forms #} +{% macro team_select(name, label, include_none, teams=none) %}
@@ -68,24 +92,32 @@
{% endmacro %} -{# Team dropdown where a value might be preselected. Requires list of teams. #} -{% macro team_select_with_preselect(match='', name='', label='') %} +{# Team select for forms where a value might be preselected #} +{% macro team_select_with_preselect(team_match, name, label, include_none, teams=none) %}
@@ -94,44 +126,43 @@ {% endmacro %} {# Easy nav-bar entries. When a page sets the active_page variable, the current entry will be underlined #} -{% macro nav_selector(page='', text='') %} +{% macro nav_selector(page, text) %} {% if active_page == page %}{% endif %} {{ text }} - {# NOTE: This should be set at the top of each template #} + {# NOTE: active_page should be set at the top of each template #} {% if active_page == page %}{% endif %} {% endmacro %} -{#@formatter:off#} -{% macro pxx_guess_colorization(driver_abbr='', result=none) -%} -{% if (driver_abbr == result.pxx_driver(-3).abbr) and (driver_abbr != "None") %}fw-bold -{% elif (driver_abbr == result.pxx_driver(-2).abbr) and (driver_abbr != "None") %}text-danger fw-bold -{% elif (driver_abbr == result.pxx_driver(-1).abbr) and (driver_abbr != "None") %}text-warning fw-bold -{% elif (driver_abbr == result.pxx_driver(0).abbr) %}text-success fw-bold -{% elif (driver_abbr == result.pxx_driver(1).abbr) and (driver_abbr != "None") %}text-warning fw-bold -{% elif (driver_abbr == result.pxx_driver(2).abbr) and (driver_abbr != "None") %}text-danger fw-bold -{% elif (driver_abbr == result.pxx_driver(3).abbr) and (driver_abbr != "None") %}fw-bold{% endif %} -{% endmacro %} - -{% macro pxx_points_tooltip_text(driver_abbr='', result=none) -%} -{% if (driver_abbr == result.pxx_driver(-3).abbr) and (driver_abbr != "None") %}1 Point -{% elif (driver_abbr == result.pxx_driver(-2).abbr) and (driver_abbr != "None") %}3 Points -{% elif (driver_abbr == result.pxx_driver(-1).abbr) and (driver_abbr != "None") %}6 Points -{% elif (driver_abbr == result.pxx_driver(0).abbr) %}10 Points -{% elif (driver_abbr == result.pxx_driver(1).abbr) and (driver_abbr != "None") %}6 Points -{% elif (driver_abbr == result.pxx_driver(2).abbr) and (driver_abbr != "None") %}3 Points -{% elif (driver_abbr == result.pxx_driver(3).abbr) and (driver_abbr != "None") %}1 Point -{% else %}0 Points{% endif %} +{% macro pxx_guess_colorization(guessed_driver, result) -%} + {% if (guessed_driver == result.offset_from_place_to_guess(-3)) and (guessed_driver != model.none_driver()) %} + fw-bold + {% elif (guessed_driver == result.offset_from_place_to_guess(-2)) and (guessed_driver != model.none_driver()) %} + text-danger fw-bold + {% elif (guessed_driver == result.offset_from_place_to_guess(-1)) and (guessed_driver != model.none_driver()) %} + text-warning fw-bold + {% elif (guessed_driver == result.offset_from_place_to_guess( 0)) %}text-success fw-bold + {% elif (guessed_driver == result.offset_from_place_to_guess( 1)) and (guessed_driver != model.none_driver()) %} + text-warning fw-bold + {% elif (guessed_driver == result.offset_from_place_to_guess( 2)) and (guessed_driver != model.none_driver()) %} + text-danger fw-bold + {% elif (guessed_driver == result.offset_from_place_to_guess( 3)) and (guessed_driver != model.none_driver()) %} + fw-bold + {% endif %} {%- endmacro %} -{% macro pxx_standing_tooltip_text(result=none) -%} -P{{ result.race.pxx - 3 }}: {{ result.pxx_driver(-3).abbr }} -P{{ result.race.pxx - 2 }}: {{ result.pxx_driver(-2).abbr }} -P{{ result.race.pxx - 1 }}: {{ result.pxx_driver(-1).abbr }} -P{{ result.race.pxx }}: {{ result.pxx_driver(0).abbr }} -P{{ result.race.pxx + 1 }}: {{ result.pxx_driver(1).abbr }} -P{{ result.race.pxx + 2 }}: {{ result.pxx_driver(2).abbr }} -P{{ result.race.pxx + 3 }}: {{ result.pxx_driver(3).abbr }} -{% endmacro %} -{#@formatter:on#} +{% macro dnf_guess_colorization(guessed_driver, result) -%} + {% if guessed_driver in result.initial_dnf %}text-success fw-bold + {% elif (guessed_driver == model.none_driver()) and (result.initial_dnf | length == 0) %}text-success fw-bold + {% endif %} +{%- endmacro %} + +{# @formatter:off #} +{% macro pxx_standing_tooltip_text(result) %} +{%- for position in range(-3, 4) %} +{%- set driver = result.offset_from_place_to_guess(position, respect_nc=false) %} +{{- driver.abbr ~ result.driver_standing_position_string(driver) }} +{% endfor %} +{%- endmacro %} +{# @formatter:on #} @@ -170,18 +201,18 @@ P{{ result.race.pxx + 3 }}: {{ result.pxx_driver(3).abbr }}
diff --git a/formula10/templates/enter.jinja b/formula10/templates/enter.jinja index 7053e07..b796009 100644 --- a/formula10/templates/enter.jinja +++ b/formula10/templates/enter.jinja @@ -3,33 +3,25 @@ {% block title %}Formula 10 - Race Result{% endblock title %} {% set active_page = "/result" %} -{% set active_user = none %} {% block head_extra %} - - + + {% endblock head_extra %} -{% set current_race = model.first_race_without_result() %} - {% block navbar_center %} {% if model.all_race_results() | length > 0 %}