diff --git a/app/database/model.py b/app/database/model.py deleted file mode 100644 index 84aba65..0000000 --- a/app/database/model.py +++ /dev/null @@ -1,490 +0,0 @@ -import json -from datetime import datetime -from typing import Any, List, Dict -from urllib.parse import quote -from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import Integer, String, DateTime, ForeignKey -from sqlalchemy.orm import Mapped, mapped_column, relationship - -db: SQLAlchemy = SQLAlchemy() - -#################################### -# Static Data (Defined in Backend) # -#################################### - - -class Race(db.Model): - """ - A single race at a certain date and GrandPrix in the calendar. - It stores the place to guess for this race. - """ - __tablename__ = "race" - - @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 - - @property - def name_sanitized(self) -> str: - 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) # This is the place to guess - - -class Team(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 - - name: Mapped[str] = mapped_column(String(32), primary_key=True) - - -class Driver(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 - - name: Mapped[str] = mapped_column(String(32), primary_key=True) - abbr: Mapped[str] = mapped_column(String(4)) - 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_name]) - - -###################################### -# Dynamic Data (Defined in Frontend) # -###################################### - - -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) - - -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 - - -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]) - - -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 - - -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 - - -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]) diff --git a/app/frontend/controller.py b/app/frontend/controller.py deleted file mode 100644 index 5876145..0000000 --- a/app/frontend/controller.py +++ /dev/null @@ -1,174 +0,0 @@ -from typing import List -from urllib.parse import unquote -from flask import Flask, render_template, request, redirect -from werkzeug import Response -from app.database.model import Team, db -from app.database.file_utils import reload_static_data, reload_dynamic_data, export_dynamic_data -from app.frontend.template_model import TemplateModel -from app.database.backend_model import delete_race_guess, update_race_guess, update_race_result, update_season_guess, update_user - -app = Flask(__name__) - -app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///formula10.db" -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -app.url_map.strict_slashes = False - -db.init_app(app) - - -# TODO -# General - -# - 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 -# - Order user table by points + display points somewhere -# - Show current values for some season guesses (e.g. current most dnfs) -# - Generate static diagram using chart.js + templating the js (funny yikes) - - -@app.route("/") -def root() -> Response: - return redirect("/race/Everyone") - - -@app.route("/save/all") -def save() -> Response: - export_dynamic_data() - return redirect("/") - - -@app.route("/load/all") -def load() -> Response: - reload_static_data() - reload_dynamic_data() - return redirect("/") - - -@app.route("/load/static") -def load_static() -> Response: - reload_static_data() - return redirect("/") - - -@app.route("/load/dynamic") -def load_dynamic() -> Response: - reload_dynamic_data() - return redirect("/") - - -@app.route("/race") -def race_root() -> Response: - return redirect("/race/Everyone") - - -@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) - - -@app.route("/race-guess//", methods=["POST"]) -def race_guess_post(race_name: str, user_name: str) -> Response: - race_name = unquote(race_name) - user_name = unquote(user_name) - - pxx: str | None = request.form.get("pxxselect") - dnf: str | None = request.form.get("dnfselect") - - return update_race_guess(race_name, user_name, pxx, dnf) - - -@app.route("/race-guess-delete//", methods=["POST"]) -def race_guess_delete_post(race_name: str, user_name: str) -> Response: - race_name = unquote(race_name) - user_name = unquote(user_name) - - return delete_race_guess(race_name, user_name) - - -@app.route("/season") -def season_root() -> Response: - return redirect("/season/Everyone") - - -@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) - - -@app.route("/season-guess/", methods=["POST"]) -def season_guess_post(user_name: str) -> Response: - user_name = unquote(user_name) - guesses: List[str | None] = [ - request.form.get("hottakeselect"), - request.form.get("p2select"), - request.form.get("overtakeselect"), - request.form.get("dnfselect"), - request.form.get("gainedselect"), - request.form.get("lostselect") - ] - team_winner_guesses: List[str | None] = [ - request.form.get(f"teamwinner-{team.name}") for team in db.session.query(Team).all() - ] - podium_driver_guesses: List[str] = request.form.getlist("podiumdrivers") - - return update_season_guess(user_name, guesses, team_winner_guesses, podium_driver_guesses) - - -@app.route("/result") -def result_root() -> Response: - return redirect("/result/Current") - - -@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) - - -@app.route("/result-enter/", methods=["POST"]) -def result_enter_post(race_name: str) -> Response: - race_name = unquote(race_name) - pxxs: List[str] = request.form.getlist("pxx-drivers") - first_dnfs: List[str] = request.form.getlist("first-dnf-drivers") - dnfs: List[str] = request.form.getlist("dnf-drivers") - excluded: List[str] = request.form.getlist("excluded-drivers") - - return update_race_result(race_name, pxxs, first_dnfs, dnfs, excluded) - - -@app.route("/user") -def user_root() -> str: - model = TemplateModel() - return render_template("users.jinja", - model=model) - - -@app.route("/user-add", methods=["POST"]) -def user_add_post() -> Response: - username: str | None = request.form.get("select-add-user") - return update_user(username, add=True) - - -@app.route("/user-delete", methods=["POST"]) -def user_delete_post() -> Response: - username: str | None = request.form.get("select-delete-user") - return update_user(username, delete=True) \ No newline at end of file diff --git a/data/dynamic_export/users.csv b/data/dynamic_export/users.csv new file mode 100644 index 0000000..2b15e00 --- /dev/null +++ b/data/dynamic_export/users.csv @@ -0,0 +1,8 @@ +name +Christoph +Angela Merkel +Xi Jinping +Donald Trump +Joe Biden +Henri +Vinzent diff --git a/static_data/drivers.csv b/data/static_import/drivers.csv similarity index 100% rename from static_data/drivers.csv rename to data/static_import/drivers.csv diff --git a/static_data/races.csv b/data/static_import/races.csv similarity index 100% rename from static_data/races.csv rename to data/static_import/races.csv diff --git a/static_data/teams.csv b/data/static_import/teams.csv similarity index 100% rename from static_data/teams.csv rename to data/static_import/teams.csv diff --git a/formula10.py b/formula10.py deleted file mode 100644 index aff3a35..0000000 --- a/formula10.py +++ /dev/null @@ -1,4 +0,0 @@ -from app.frontend.controller import app - -if __name__ == "__main__": - app.run(debug=True, host="0.0.0.0") \ No newline at end of file diff --git a/formula10/__init__.py b/formula10/__init__.py new file mode 100644 index 0000000..b56711f --- /dev/null +++ b/formula10/__init__.py @@ -0,0 +1,41 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy + +app: Flask = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///formula10.db" +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.url_map.strict_slashes = False + +db: SQLAlchemy = SQLAlchemy() +db.init_app(app) + +# NOTE: These imports are required to register the routes. They need to be after "app" is declared +import formula10.controller.race_controller # type: ignore +import formula10.controller.season_controller # type: ignore +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 +# - Order user table by points + display points somewhere +# - Show current values for some season guesses (e.g. current most dnfs) +# - Generate static diagram using chart.js + templating the js (funny yikes) + + +# if __name__ == "__main__": + # app.run(debug=True, host="0.0.0.0") \ No newline at end of file diff --git a/formula10/controller/admin_controller.py b/formula10/controller/admin_controller.py new file mode 100644 index 0000000..029b9fd --- /dev/null +++ b/formula10/controller/admin_controller.py @@ -0,0 +1,78 @@ +from typing import List +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.frontend.template_model import TemplateModel +from formula10 import app + + +@app.route("/save/all") +def save() -> Response: + export_dynamic_data() + return redirect("/") + + +@app.route("/load/all") +def load() -> Response: + reload_static_data() + reload_dynamic_data() + return redirect("/") + + +@app.route("/load/static") +def load_static() -> Response: + reload_static_data() + return redirect("/") + + +@app.route("/load/dynamic") +def load_dynamic() -> Response: + reload_dynamic_data() + return redirect("/") + + +@app.route("/result") +def result_root() -> Response: + return redirect("/result/Current") + + +@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) + + +@app.route("/result-enter/", methods=["POST"]) +def result_enter_post(race_name: str) -> Response: + race_name = unquote(race_name) + pxxs: List[str] = request.form.getlist("pxx-drivers") + first_dnfs: List[str] = request.form.getlist("first-dnf-drivers") + dnfs: List[str] = request.form.getlist("dnf-drivers") + excluded: List[str] = request.form.getlist("excluded-drivers") + + return update_race_result(race_name, pxxs, first_dnfs, dnfs, excluded) + + +@app.route("/user") +def user_root() -> str: + model = TemplateModel() + return render_template("users.jinja", + model=model) + + +@app.route("/user-add", methods=["POST"]) +def user_add_post() -> Response: + username: str | None = request.form.get("select-add-user") + return update_user(username, add=True) + + +@app.route("/user-delete", methods=["POST"]) +def user_delete_post() -> Response: + username: str | None = request.form.get("select-delete-user") + return update_user(username, delete=True) \ No newline at end of file diff --git a/formula10/controller/race_controller.py b/formula10/controller/race_controller.py new file mode 100644 index 0000000..2e5d0de --- /dev/null +++ b/formula10/controller/race_controller.py @@ -0,0 +1,45 @@ +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.frontend.template_model import TemplateModel +from formula10 import app + + +@app.route("/") +def root() -> Response: + return redirect("/race/Everyone") + + +@app.route("/race") +def race_root() -> Response: + return redirect("/race/Everyone") + + +@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) + + +@app.route("/race-guess//", methods=["POST"]) +def race_guess_post(race_name: str, user_name: str) -> Response: + race_name = unquote(race_name) + user_name = unquote(user_name) + + pxx: str | None = request.form.get("pxxselect") + dnf: str | None = request.form.get("dnfselect") + + return update_race_guess(race_name, user_name, pxx, dnf) + + +@app.route("/race-guess-delete//", methods=["POST"]) +def race_guess_delete_post(race_name: str, user_name: str) -> Response: + race_name = unquote(race_name) + user_name = unquote(user_name) + + return delete_race_guess(race_name, user_name) \ No newline at end of file diff --git a/formula10/controller/season_controller.py b/formula10/controller/season_controller.py new file mode 100644 index 0000000..c349f12 --- /dev/null +++ b/formula10/controller/season_controller.py @@ -0,0 +1,43 @@ +from typing import List +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.frontend.template_model import TemplateModel +from formula10 import app, db + + +@app.route("/season") +def season_root() -> Response: + return redirect("/season/Everyone") + + +@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) + + +@app.route("/season-guess/", methods=["POST"]) +def season_guess_post(user_name: str) -> Response: + user_name = unquote(user_name) + guesses: List[str | None] = [ + request.form.get("hottakeselect"), + request.form.get("p2select"), + request.form.get("overtakeselect"), + request.form.get("dnfselect"), + request.form.get("gainedselect"), + request.form.get("lostselect") + ] + # 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() + ] + podium_driver_guesses: List[str] = request.form.getlist("podiumdrivers") + + return update_season_guess(user_name, guesses, team_winner_guesses, podium_driver_guesses) \ No newline at end of file diff --git a/app/database/database_utils.py b/formula10/database/common_query_util.py similarity index 66% rename from app/database/database_utils.py rename to formula10/database/common_query_util.py index de923af..03775b6 100644 --- a/app/database/database_utils.py +++ b/formula10/database/common_query_util.py @@ -1,5 +1,6 @@ -from app.database.model import User, db, RaceResult - +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 diff --git a/app/database/file_utils.py b/formula10/database/import_export_util.py similarity index 62% rename from app/database/file_utils.py rename to formula10/database/import_export_util.py index 1c7b222..8603d17 100644 --- a/app/database/file_utils.py +++ b/formula10/database/import_export_util.py @@ -1,7 +1,17 @@ import csv import os.path from typing import List, Any -from app.database.model import Team, Driver, Race, User, RaceResult, RaceGuess, TeamWinners, PodiumDrivers, SeasonGuess, db + +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 def load_csv(filename: str) -> List[List[str]]: @@ -39,11 +49,11 @@ def reload_static_data(): db.session.query(Race).delete() # Reload static data - for row in load_csv("static_data/teams.csv"): + for row in load_csv("../../data/static_import/teams.csv"): db.session.add(Team.from_csv(row)) - for row in load_csv("static_data/drivers.csv"): + for row in load_csv("../../data/static_import/drivers.csv"): db.session.add(Driver.from_csv(row)) - for row in load_csv("static_data/races.csv"): + for row in load_csv("../../data/static_import/races.csv"): db.session.add(Race.from_csv(row)) db.session.commit() @@ -63,17 +73,17 @@ def reload_dynamic_data(): db.session.query(SeasonGuess).delete() # Reload dynamic data - for row in load_csv("dynamic_data/users.csv"): + for row in load_csv("../../data/dynamic_export/users.csv"): db.session.add(User.from_csv(row)) - for row in load_csv("dynamic_data/raceresults.csv"): + for row in load_csv("../../data/dynamic_export/raceresults.csv"): db.session.add(RaceResult.from_csv(row)) - for row in load_csv("dynamic_data/raceguesses.csv"): + for row in load_csv("../../data/dynamic_export/raceguesses.csv"): db.session.add(RaceGuess.from_csv(row)) - for row in load_csv("dynamic_data/teamwinners.csv"): + for row in load_csv("../../data/dynamic_export/teamwinners.csv"): db.session.add(TeamWinners.from_csv(row)) - for row in load_csv("dynamic_data/podiumdrivers.csv"): + for row in load_csv("../../data/dynamic_export/podiumdrivers.csv"): db.session.add(PodiumDrivers.from_csv(row)) - for row in load_csv("dynamic_data/seasonguesses.csv"): + for row in load_csv("../../data/dynamic_export/seasonguesses.csv"): db.session.add(SeasonGuess.from_csv(row)) db.session.commit() @@ -89,9 +99,9 @@ def export_dynamic_data(): podiumdrivers: List[PodiumDrivers] = db.session.query(PodiumDrivers).all() seasonguesses: List[SeasonGuess] = db.session.query(SeasonGuess).all() - write_csv("dynamic_data/users.csv", users) - write_csv("dynamic_data/raceresults.csv", raceresults) - write_csv("dynamic_data/raceguesses.csv", raceguesses) - write_csv("dynamic_data/teamwinners.csv", teamwinners) - write_csv("dynamic_data/podiumdrivers.csv", podiumdrivers) - write_csv("dynamic_data/seasonguesses.csv", seasonguesses) + 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/driver.py new file mode 100644 index 0000000..fd913dd --- /dev/null +++ b/formula10/database/model/driver.py @@ -0,0 +1,31 @@ +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 import db + + +class Driver(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 + + name: Mapped[str] = mapped_column(String(32), primary_key=True) + abbr: Mapped[str] = mapped_column(String(4)) + 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_name]) \ No newline at end of file diff --git a/formula10/database/model/podium_drivers.py b/formula10/database/model/podium_drivers.py new file mode 100644 index 0000000..b1ae4b0 --- /dev/null +++ b/formula10/database/model/podium_drivers.py @@ -0,0 +1,60 @@ +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.py b/formula10/database/model/race.py new file mode 100644 index 0000000..704a513 --- /dev/null +++ b/formula10/database/model/race.py @@ -0,0 +1,33 @@ +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): + """ + 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 + + @property + def name_sanitized(self) -> str: + 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) # This is the place to guess \ No newline at end of file diff --git a/formula10/database/model/race_guess.py b/formula10/database/model/race_guess.py new file mode 100644 index 0000000..7d2e90f --- /dev/null +++ b/formula10/database/model/race_guess.py @@ -0,0 +1,46 @@ +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 new file mode 100644 index 0000000..cd84ed7 --- /dev/null +++ b/formula10/database/model/race_result.py @@ -0,0 +1,171 @@ +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 new file mode 100644 index 0000000..5319936 --- /dev/null +++ b/formula10/database/model/season_guess.py @@ -0,0 +1,81 @@ +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.py b/formula10/database/model/team.py new file mode 100644 index 0000000..79506f4 --- /dev/null +++ b/formula10/database/model/team.py @@ -0,0 +1,19 @@ +from typing import List +from sqlalchemy import String +from sqlalchemy.orm import Mapped, mapped_column + +from formula10 import db + +class Team(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 + + name: Mapped[str] = mapped_column(String(32), primary_key=True) \ No newline at end of file diff --git a/formula10/database/model/team_winners.py b/formula10/database/model/team_winners.py new file mode 100644 index 0000000..f154918 --- /dev/null +++ b/formula10/database/model/team_winners.py @@ -0,0 +1,59 @@ +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 new file mode 100644 index 0000000..4173332 --- /dev/null +++ b/formula10/database/model/user.py @@ -0,0 +1,32 @@ +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/app/database/backend_model.py b/formula10/database/update_query_util.py similarity index 94% rename from app/database/backend_model.py rename to formula10/database/update_query_util.py index 87b89ce..5a54802 100644 --- a/app/database/backend_model.py +++ b/formula10/database/update_query_util.py @@ -2,9 +2,16 @@ from typing import Dict, List, cast from urllib.parse import quote from flask import redirect from werkzeug import Response -from app.database.database_utils import race_has_result, user_exists -from app.database.model import PodiumDrivers, RaceResult, SeasonGuess, TeamWinners, User, db, RaceGuess -from app.database.validation_utils import any_is_none, positions_are_contiguous + +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 import db def find_or_create_race_guess(user_name: str, race_name: str) -> RaceGuess: diff --git a/app/database/validation_utils.py b/formula10/database/validation_util.py similarity index 100% rename from app/database/validation_utils.py rename to formula10/database/validation_util.py diff --git a/app/frontend/template_model.py b/formula10/frontend/template_model.py similarity index 94% rename from app/frontend/template_model.py rename to formula10/frontend/template_model.py index 7af7798..4a59a0a 100644 --- a/app/frontend/template_model.py +++ b/formula10/frontend/template_model.py @@ -1,7 +1,15 @@ from typing import List, Callable, Dict, overload from sqlalchemy import desc -from app.database.model import User, RaceResult, RaceGuess, Race, Driver, Team, SeasonGuess, db -from app.database.validation_utils import find_first_or_none, find_multiple, find_single, find_single_or_none + +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 import db # This could also be moved to database_utils (at least partially), but I though the template should cache the database responses diff --git a/app/logic/points_model.py b/formula10/logic/points_model.py similarity index 100% rename from app/logic/points_model.py rename to formula10/logic/points_model.py diff --git a/static/image/f1_logo.svg b/formula10/static/image/f1_logo.svg similarity index 100% rename from static/image/f1_logo.svg rename to formula10/static/image/f1_logo.svg diff --git a/static/image/favicon.svg b/formula10/static/image/favicon.svg similarity index 100% rename from static/image/favicon.svg rename to formula10/static/image/favicon.svg diff --git a/static/script/bootstrap.bundle.js b/formula10/static/script/bootstrap.bundle.js similarity index 100% rename from static/script/bootstrap.bundle.js rename to formula10/static/script/bootstrap.bundle.js diff --git a/static/script/bootstrap.bundle.min.js b/formula10/static/script/bootstrap.bundle.min.js similarity index 100% rename from static/script/bootstrap.bundle.min.js rename to formula10/static/script/bootstrap.bundle.min.js diff --git a/static/script/draggable.js b/formula10/static/script/draggable.js similarity index 100% rename from static/script/draggable.js rename to formula10/static/script/draggable.js diff --git a/static/style/bootstrap.css b/formula10/static/style/bootstrap.css similarity index 100% rename from static/style/bootstrap.css rename to formula10/static/style/bootstrap.css diff --git a/static/style/bootstrap.css.map b/formula10/static/style/bootstrap.css.map similarity index 100% rename from static/style/bootstrap.css.map rename to formula10/static/style/bootstrap.css.map diff --git a/static/style/bootstrap.scss b/formula10/static/style/bootstrap.scss similarity index 100% rename from static/style/bootstrap.scss rename to formula10/static/style/bootstrap.scss diff --git a/static/style/draggable.css b/formula10/static/style/draggable.css similarity index 100% rename from static/style/draggable.css rename to formula10/static/style/draggable.css diff --git a/templates/base.jinja b/formula10/templates/base.jinja similarity index 100% rename from templates/base.jinja rename to formula10/templates/base.jinja diff --git a/templates/enter.jinja b/formula10/templates/enter.jinja similarity index 97% rename from templates/enter.jinja rename to formula10/templates/enter.jinja index 8c3f119..7053e07 100644 --- a/templates/enter.jinja +++ b/formula10/templates/enter.jinja @@ -6,8 +6,8 @@ {% set active_user = none %} {% block head_extra %} - - + + {% endblock head_extra %} {% set current_race = model.first_race_without_result() %} diff --git a/templates/race.jinja b/formula10/templates/race.jinja similarity index 100% rename from templates/race.jinja rename to formula10/templates/race.jinja diff --git a/templates/season.jinja b/formula10/templates/season.jinja similarity index 100% rename from templates/season.jinja rename to formula10/templates/season.jinja diff --git a/templates/users.jinja b/formula10/templates/users.jinja similarity index 100% rename from templates/users.jinja rename to formula10/templates/users.jinja