diff --git a/backend_model.py b/backend_model.py index c6c2729..100d8c9 100644 --- a/backend_model.py +++ b/backend_model.py @@ -48,6 +48,17 @@ def update_race_guess(race_name: str, user_name: str, pxx_select: str | None, dn return redirect("/race/Everyone") +def delete_race_guess(race_name: str, user_name: str) -> Response: + # Don't change guesses that are already over + 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.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() @@ -112,7 +123,9 @@ def find_or_create_season_guess(user_name: str) -> SeasonGuess: return season_guess -def update_season_guess(user_name: str, guesses: List[str | None] | List[str], team_winner_guesses: List[str | None] | List[str], podium_driver_guesses: List[str]) -> Response: +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.hot_take = guesses[0] season_guess.p2_team_name = guesses[1] @@ -125,7 +138,7 @@ def update_season_guess(user_name: str, guesses: List[str | None] | List[str], t db.session.commit() - return redirect(f"/season/{quote(user_name)}") + return redirect(f"/season/Everyone") def find_or_create_race_result(race_name: str) -> RaceResult: @@ -146,53 +159,30 @@ def find_or_create_race_result(race_name: str) -> RaceResult: return race_result -def update_race_result(race_name: str, pxx_driver_names_list: List[str], dnf_driver_names_list: List[str], excluded_driver_names_list: List[str]) -> Response: +def update_race_result(race_name: str, pxx_driver_names_list: List[str], first_dnf_driver_names_list: List[str], dnf_driver_names_list: List[str], excluded_driver_names_list: List[str]) -> Response: # Use strings as keys, as these dicts will be serialized to json - # The pxx_driver_names_list contains all 20 drivers, so use that one to determine positions for dnf_driver_names and excluded_driver_names pxx_driver_names: Dict[str, str] = { str(position + 1): driver for position, driver in enumerate(pxx_driver_names_list) - if driver not in dnf_driver_names_list and driver not in excluded_driver_names_list - } - dnf_driver_names: Dict[str, str] = { - str(position + 1): driver for position, driver in enumerate(pxx_driver_names_list) - if driver in dnf_driver_names_list and driver not in excluded_driver_names_list } + + # Not counted drivers have to be at the end excluded_driver_names: Dict[str, str] = { str(position + 1): driver for position, driver in enumerate(pxx_driver_names_list) if driver in excluded_driver_names_list } - - # All dictionaries should be disjunct and result in a complete list from P1-P20 if merged - union: Dict[str, str] = pxx_driver_names | dnf_driver_names | excluded_driver_names - if len(union) != 20 or not positions_are_contiguous(list(union.keys())) or "1" not in union or "20" not in union: + if len(excluded_driver_names) > 0 and (not "20" in excluded_driver_names or not positions_are_contiguous(list(excluded_driver_names.keys()))): return redirect(f"/result/{quote(race_name)}") - # dnf_drivers have positions above excluded_drivers - if len(excluded_driver_names) > 0: - best_excluded_driver_position: int = min(map(int, excluded_driver_names.keys())) - for position in dnf_driver_names.keys(): - if int(position) >= best_excluded_driver_position: - return redirect(f"/result/{quote(race_name)}") - - # pxx_drivers have positions above dnf_drivers - if len(dnf_driver_names) > 0: - best_dnf_driver_position: int = min(map(int, dnf_driver_names.keys())) - for position in pxx_driver_names.keys(): - if int(position) >= best_dnf_driver_position: - return redirect(f"/result/{quote(race_name)}") - - # pxx_drivers have positions above excluded_drivers - if len(excluded_driver_names) > 0: - best_excluded_driver_position: int = min(map(int, excluded_driver_names.keys())) - for position in pxx_driver_names.keys(): - if int(position) >= best_excluded_driver_position: - return redirect(f"/result/{quote(race_name)}") - + # First DNF drivers have to be contained in DNF drivers + for driver_name in first_dnf_driver_names_list: + 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.dnf_driver_names = dnf_driver_names - race_result.excluded_driver_names = excluded_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 db.session.commit() diff --git a/formula10.py b/formula10.py index 0e35455..4df3a43 100644 --- a/formula10.py +++ b/formula10.py @@ -5,7 +5,7 @@ from werkzeug import Response from model import Team, db from file_utils import reload_static_data, reload_dynamic_data, export_dynamic_data from template_model import TemplateModel -from backend_model import update_race_guess, update_race_result, update_season_guess, update_user +from backend_model import delete_race_guess, update_race_guess, update_race_result, update_season_guess, update_user app = Flask(__name__) @@ -19,11 +19,12 @@ db.init_app(app) # TODO # General -# - Show place when entering race result (would require updating the drag'n'drop code...) +# - Choose "place to guess" late before the race? Make a page for this +# - 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? -# - Choose "place to guess" late before the race? # Statistics # - Auto calculate points @@ -89,6 +90,14 @@ def race_guess_post(race_name: str, user_name: str) -> Response: 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") @@ -139,11 +148,12 @@ def result_active_race(race_name: str) -> str: @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("pxxdrivers") + 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") - excludes: List[str] = request.form.getlist("exclude-drivers") + excluded: List[str] = request.form.getlist("excluded-drivers") - return update_race_result(race_name, pxxs, dnfs, excludes) + return update_race_result(race_name, pxxs, first_dnfs, dnfs, excluded) @app.route("/user") diff --git a/model.py b/model.py index 0b76cea..84aba65 100644 --- a/model.py +++ b/model.py @@ -71,7 +71,7 @@ class Driver(db.Model): return driver name: Mapped[str] = mapped_column(String(32), primary_key=True) - abbr: Mapped[str] = mapped_column(String(3)) + 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 @@ -118,7 +118,7 @@ class RaceResult(db.Model): """ __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", "dnf_driver_names_json", "excluded_driver_names_json"] + __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 @@ -127,20 +127,23 @@ class RaceResult(db.Model): def from_csv(row: List[str]): race_result: RaceResult = RaceResult(str(row[0])) race_result.pxx_driver_names_json = str(row[1]) - race_result.dnf_driver_names_json = str(row[2]) - race_result.excluded_driver_names_json = str(row[3]) + 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) @@ -153,26 +156,35 @@ class RaceResult(db.Model): self.pxx_driver_names_json = json.dumps(new_pxx_driver_names) @property - def dnf_driver_names(self) -> Dict[str, str]: + 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: Dict[str, str]): + 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) -> Dict[str, str]: + 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: Dict[str, str]): + 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 - _dnf_drivers: Dict[str, Driver] | None = None - _excluded_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]: @@ -187,70 +199,81 @@ class RaceResult(db.Model): return self._pxx_drivers - @property - def dnf_drivers(self) -> Dict[str, Driver]: - if self._dnf_drivers is None: - self._dnf_drivers = dict() - for position, driver_name in self.dnf_driver_names.items(): - driver: Driver | 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[position] = driver - - return self._dnf_drivers - - @property - def excluded_drivers(self) -> Dict[str, Driver]: - if self._excluded_drivers is None: - self._excluded_drivers = dict() - for position, driver_name in self.excluded_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._excluded_drivers[position] = driver - - return self._excluded_drivers - - def pxx(self, offset: int = 0) -> Driver | None: + 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: - none_driver: Driver | None = db.session.query(Driver).filter_by(name="NONE").first() + 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("NONE-driver not found in database") + raise Exception(f"NONE-driver not found in database") return none_driver + return self.pxx_drivers[pxx_num] - @property - def dnf(self) -> Driver: - 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") + 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 sorted(self.dnf_drivers.items(), reverse=True)[0][1] if len(self.dnf_drivers) > 0 else none_driver - - def single_position(self, position: str) -> Driver: - if position in self.pxx_drivers: - return self.pxx_drivers[position] - - if position in self.dnf_drivers: - return self.dnf_drivers[position] - - if position in self.excluded_drivers: - return self.excluded_drivers[position] - - raise Exception(f"Driver for position {position} not found in pxx/dnf/excluded") + return "NC" @property def all_positions(self) -> List[Driver]: return [ - self.single_position(str(position)) for position in range(1, 21) + 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): """ diff --git a/template_model.py b/template_model.py index 5dfee4e..c3d7784 100644 --- a/template_model.py +++ b/template_model.py @@ -215,7 +215,7 @@ class TemplateModel: """ Returns a list of all drivers in the database, excluding the NONE driver. """ - predicate: Callable[[Driver], bool] = lambda driver: driver.name != "NONE" + predicate: Callable[[Driver], bool] = lambda driver: driver.name != "None" return find_multiple(predicate, self.all_drivers()) @overload diff --git a/templates/base.jinja b/templates/base.jinja index 3d473d4..618d7a1 100644 --- a/templates/base.jinja +++ b/templates/base.jinja @@ -41,7 +41,7 @@ {% endif %} - {% if (include_none == true) and (driver.abbr == "NON") %} + {% if (include_none == true) and (driver.abbr == "None") %} {% endif %} {% endfor %} @@ -102,34 +102,34 @@ {#@formatter:off#} {% macro pxx_guess_colorization(driver_abbr='', result=none) -%} -{% if (driver_abbr == result.pxx(-3).abbr) and (driver_abbr != "NON") %}fw-bold -{% elif (driver_abbr == result.pxx(-2).abbr) and (driver_abbr != "NON") %}text-danger fw-bold -{% elif (driver_abbr == result.pxx(-1).abbr) and (driver_abbr != "NON") %}text-warning fw-bold -{% elif (driver_abbr == result.pxx(0).abbr) %}text-success fw-bold -{% elif (driver_abbr == result.pxx(1).abbr) and (driver_abbr != "NON") %}text-warning fw-bold -{% elif (driver_abbr == result.pxx(2).abbr) and (driver_abbr != "NON") %}text-danger fw-bold -{% elif (driver_abbr == result.pxx(3).abbr) and (driver_abbr != "NON") %}fw-bold{% endif %} +{% 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(-3).abbr) and (driver_abbr != "NON") %}1 Point -{% elif (driver_abbr == result.pxx(-2).abbr) and (driver_abbr != "NON") %}3 Points -{% elif (driver_abbr == result.pxx(-1).abbr) and (driver_abbr != "NON") %}6 Points -{% elif (driver_abbr == result.pxx(0).abbr) %}10 Points -{% elif (driver_abbr == result.pxx(1).abbr) and (driver_abbr != "NON") %}6 Points -{% elif (driver_abbr == result.pxx(2).abbr) and (driver_abbr != "NON") %}3 Points -{% elif (driver_abbr == result.pxx(3).abbr) and (driver_abbr != "NON") %}1 Point +{% 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 %} {%- endmacro %} {% macro pxx_standing_tooltip_text(result=none) -%} -P{{ result.race.pxx - 3 }}: {{ result.pxx(-3).abbr }} -P{{ result.race.pxx - 2 }}: {{ result.pxx(-2).abbr }} -P{{ result.race.pxx - 1 }}: {{ result.pxx(-1).abbr }} -P{{ result.race.pxx }}: {{ result.pxx(0).abbr }} -P{{ result.race.pxx + 1 }}: {{ result.pxx(1).abbr }} -P{{ result.race.pxx + 2 }}: {{ result.pxx(2).abbr }} -P{{ result.race.pxx + 3 }}: {{ result.pxx(3).abbr }} +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#} diff --git a/templates/enter.jinja b/templates/enter.jinja index dc6397f..8c3f119 100644 --- a/templates/enter.jinja +++ b/templates/enter.jinja @@ -82,28 +82,37 @@ {{ driver.name }}
- {# Driver DNFed #} + {# Driver DNFed at first #}
+ + +
+ + {# Driver DNFed #} +
+ {% if (active_result is not none) and (driver in active_result.dnf_drivers) %}checked{% endif %}>
{# Driver Excluded #} -
+
+ id="exclude-{{ driver.name }}" name="excluded-drivers" + {% if (active_result is not none) and (driver in active_result.excluded_drivers) %}checked{% endif %}> + title="Driver is not counted for standing">NC
{# Standing order #} - + {% endfor %} diff --git a/templates/race.jinja b/templates/race.jinja index c6a66cb..e94c4dc 100644 --- a/templates/race.jinja +++ b/templates/race.jinja @@ -70,8 +70,7 @@ - {{ current_race.number }}: {{ current_race.name }}
+ {{ current_race.number }}: {{ current_race.name }}
Guess: P{{ current_race.pxx }} @@ -82,10 +81,10 @@ {% if user_guess is not none %}
  • - P{{ current_race.pxx }}: {{ user_guess.pxx.abbr }} + {{ user_guess.pxx.abbr }}
  • - DNF: {{ user_guess.dnf.abbr }} + {{ user_guess.dnf.abbr }}
{% else %} @@ -120,6 +119,9 @@ +
+ +
  @@ -130,8 +132,7 @@ {% for past_result in model.all_race_results() %} - {{ past_result.race.number }}: {{ past_result.race.name }}
+ {{ past_result.race.number }}: {{ past_result.race.name }}
Guessed: P{{ past_result.race.pxx }} @@ -149,12 +150,12 @@
  • - P{{ past_result.race.pxx }}: {{ user_guess.pxx.abbr }} + {{ user_guess.pxx.abbr }}{% if user_guess.pxx.abbr != "None" %} ({{ past_result.pxx_driver_position_string(user_guess.pxx.name) }}){% endif %}
  • -
  • - - DNF: {{ user_guess.dnf.abbr }} +
  • + + {{ user_guess.dnf.abbr }}
@@ -168,11 +169,11 @@
  • - P{{ past_result.race.pxx }}: {{ past_result.pxx().abbr }} + P{{ past_result.race.pxx }}: {{ past_result.pxx_driver().abbr }}
  • -
  • - DNF: {{ past_result.dnf.abbr }}
  • +
  • + DNF: {% for dnf_driver in past_result.first_dnf_drivers %}{{ dnf_driver.abbr }} {% endfor %}
diff --git a/templates/season.jinja b/templates/season.jinja index ccdca3a..593bc84 100644 --- a/templates/season.jinja +++ b/templates/season.jinja @@ -82,7 +82,7 @@
{# Most Gained + Lost #} -
+
{{ driver_select_with_preselect(user_guess.gained_driver.abbr if user_guess is not none else "", "gainedselect", "Most WDC places gained:", false) }} {{ driver_select_with_preselect(user_guess.lost_driver.abbr if user_guess is not none else "",