diff --git a/formula10/database/update_queries.py b/formula10/database/update_queries.py index 9a04ce4..8662b8a 100644 --- a/formula10/database/update_queries.py +++ b/formula10/database/update_queries.py @@ -3,13 +3,14 @@ from typing import Dict, List, cast from urllib.parse import quote from flask import redirect from werkzeug import Response +from formula10.controller.error_controller import error_redirect 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.database.validation import any_is_none, positions_are_contiguous, race_has_started from formula10 import db @@ -34,17 +35,17 @@ def find_or_create_race_guess(user_name: str, race_name: str) -> DbRaceGuess: def update_race_guess(race_name: str, user_name: str, pxx_select: str | None, dnf_select: str | None) -> Response: if any_is_none(pxx_select, dnf_select): - return redirect(f"/race/{quote(user_name)}") + return error_redirect(f"Picks for race \"{race_name}\" were not saved, because you did not fill all the fields.") + + if race_has_started(race_name=race_name): + return error_redirect(f"No picks for race \"{race_name}\" can be entered, as this race has already started.") + + if race_has_result(race_name): + return error_redirect(f"No picks for race \"{race_name}\" can be entered, as this race has already finished.") pxx_driver_name: str = cast(str, pxx_select) dnf_driver_name: str = cast(str, dnf_select) - # TODO: Date-lock this. Otherwise there is a period of time after the race - # but before the result where guesses can still be entered - # We can't guess for races that are already over - if race_has_result(race_name): - return redirect(f"/race/{quote(user_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 @@ -56,8 +57,11 @@ def update_race_guess(race_name: str, user_name: str, pxx_select: str | None, dn def delete_race_guess(race_name: str, user_name: str) -> Response: # Don't change guesses that are already over + if race_has_started(race_name=race_name): + return error_redirect(f"No picks for race \"{race_name}\" can be deleted, as this race has already started.") + if race_has_result(race_name): - return redirect(f"/race/{quote(user_name)}") + return error_redirect(f"No picks for race \"{race_name}\" can be deleted, as this race has already finished.") db.session.query(DbRaceGuess).filter_by(race_name=race_name, user_name=user_name).delete() db.session.commit() @@ -87,6 +91,9 @@ def find_or_create_season_guess(user_name: str) -> DbSeasonGuess: 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. + if race_has_started(race_name="Bahrain"): + return error_redirect("No season picks can be entered, as the season has already begun!") + 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 @@ -125,6 +132,9 @@ def find_or_create_race_result(race_name: str) -> DbRaceResult: 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: + if not race_has_started(race_name=race_name): + return error_redirect("No race result can be entered, as the race has not begun!") + # Use strings as keys, as these dicts will be serialized to json pxx_driver_names: Dict[str, str] = { str(position + 1): driver for position, driver in enumerate(pxx_driver_names_list) @@ -136,7 +146,7 @@ def update_race_result(race_name: str, pxx_driver_names_list: List[str], first_d if driver in excluded_driver_names_list } 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)}") + return error_redirect("Race result was not saved, as excluded drivers must be contiguous and at the end of the field!") # First DNF drivers have to be contained in DNF drivers for driver_name in first_dnf_driver_names_list: @@ -145,7 +155,7 @@ def update_race_result(race_name: str, pxx_driver_names_list: List[str], first_d # 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)}") + return error_redirect("Race result was not saved, as there cannot be DNFs without (an) initial DNF(s)!") race_result: DbRaceResult = find_or_create_race_result(race_name) race_result.pxx_driver_names_json = json.dumps(pxx_driver_names) @@ -159,18 +169,21 @@ def update_race_result(race_name: str, pxx_driver_names_list: List[str], first_d def update_user(user_name: str | None, add: bool = False, delete: bool = False) -> Response: - if user_name is None or len(user_name) < 3: - return redirect("/user") + if user_name is None: + return error_redirect("Invalid request: Cannot add/delete user because it is \"None\"!") if not add and not delete: - return redirect("/user") + return error_redirect("Invalid request: Can either add or delete user!") if add and delete: - return redirect("/user") + return error_redirect("Invalid request: Can either add or delete user!") if add: + if len(user_name) < 3: + return error_redirect(f"User \"{user_name}\" was not added, because the username must contain at least 3 characters!") + if user_exists_and_enabled(user_name): - return redirect("/user") + return error_redirect(f"User \"{user_name}\" was not added, because it already exists!") elif user_exists_and_disabled(user_name): disabled_user: DbUser | None = db.session.query(DbUser).filter_by(name=user_name, enabled=False).first() @@ -188,7 +201,10 @@ def update_user(user_name: str | None, add: bool = False, delete: bool = False) return redirect("/user") if delete: - if user_exists_and_enabled(user_name): + if user_exists_and_disabled(user_name): + return error_redirect(f"User \"{user_name}\" was not deleted, because it does not exist!") + + elif 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") @@ -196,6 +212,9 @@ def update_user(user_name: str | None, add: bool = False, delete: bool = False) enabled_user.enabled = False db.session.commit() + else: + return error_redirect(f"User \"{user_name}\" was not deleted, because it does not exist!") + return redirect("/user") raise Exception("update_user received illegal combination of arguments") diff --git a/formula10/database/validation.py b/formula10/database/validation.py index 78fab87..3a0c50a 100644 --- a/formula10/database/validation.py +++ b/formula10/database/validation.py @@ -1,4 +1,9 @@ -from typing import Any, Callable, Iterable, List, TypeVar +from datetime import datetime +from typing import Any, Callable, Iterable, List, TypeVar, overload + +from formula10.database.model.db_race import DbRace +from formula10 import db +from formula10.frontend.model.race import Race _T = TypeVar("_T") @@ -21,6 +26,28 @@ def positions_are_contiguous(positions: List[str]) -> bool: # [2, 3, 4, 5]: 2 + 3 == 5 return positions_sorted[0] + len(positions_sorted) - 1 == positions_sorted[-1] +@overload +def race_has_started(*, race: Race) -> bool: + return race_has_started(race=race) + +@overload +def race_has_started(*, race_name: str) -> bool: + return race_has_started(race_name=race_name) + +def race_has_started(*, race: Race | None = None, race_name: str | None = None) -> bool: + if race is None and race_name is not None: + _race: DbRace | None = db.session.query(DbRace).filter_by(name=race_name).first() + + if _race is None: + raise Exception(f"Couldn't obtain race {race_name} to check date") + + return datetime.now() > _race.date + + if race is not None and race_name is None: + return datetime.now() > race.date + + raise Exception("race_has_started received illegal arguments") + def find_first_else_none(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T | None: """ diff --git a/formula10/frontend/template_model.py b/formula10/frontend/template_model.py index 53db508..f2a74e5 100644 --- a/formula10/frontend/template_model.py +++ b/formula10/frontend/template_model.py @@ -15,7 +15,7 @@ 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.database.validation import find_first_else_none, find_multiple_strict, find_single_strict, find_single_or_none_strict, race_has_started from formula10 import db @@ -35,6 +35,7 @@ class TemplateModel: active_user: User | None = None active_result: RaceResult | None = None + # RIC is excluded, since he didn't drive as many races 2023 as the others _wdc_gained_excluded_abbrs: List[str] = ["RIC"] def __init__(self, *, active_user_name: str | None, active_result_race_name: str | None): @@ -42,7 +43,16 @@ class TemplateModel: 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) + if active_result_race_name == "Current": + self.active_result = self.all_race_results()[0] + else: + self.active_result = self.race_result_by(race_name=active_result_race_name) + + def race_guess_open(self, race: Race) -> bool: + return not race_has_started(race=race) + + def season_guess_open(self) -> bool: + return not race_has_started(race_name="Bahrain") def active_user_name_or_everyone(self) -> str: return self.active_user.name if self.active_user is not None else "Everyone" @@ -255,16 +265,16 @@ class TemplateModel: 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 + + raise Exception("active_result_name_or_current_race_name called without active_result or current_race") 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 + + raise Exception("active_result_race_name_or_current_race_name_sanitized called without active_result or current_race") def all_teams(self, *, include_none: bool) -> List[Team]: """ diff --git a/formula10/templates/base.jinja b/formula10/templates/base.jinja index 824008c..7a22bac 100644 --- a/formula10/templates/base.jinja +++ b/formula10/templates/base.jinja @@ -189,7 +189,7 @@