Date-lock race+season guesses + use errorpage more often
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 17s

This commit is contained in:
2024-02-26 22:15:08 +01:00
parent 2a8c17633e
commit 97d67d49ce
7 changed files with 112 additions and 39 deletions

View File

@ -3,13 +3,14 @@ from typing import Dict, List, cast
from urllib.parse import quote from urllib.parse import quote
from flask import redirect from flask import redirect
from werkzeug import Response 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.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_guess import DbRaceGuess
from formula10.database.model.db_race_result import DbRaceResult from formula10.database.model.db_race_result import DbRaceResult
from formula10.database.model.db_season_guess import DbSeasonGuess from formula10.database.model.db_season_guess import DbSeasonGuess
from formula10.database.model.db_user import DbUser 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 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: 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): 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) pxx_driver_name: str = cast(str, pxx_select)
dnf_driver_name: str = cast(str, dnf_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: DbRaceGuess = find_or_create_race_guess(user_name, race_name)
race_guess.pxx_driver_name = pxx_driver_name race_guess.pxx_driver_name = pxx_driver_name
race_guess.dnf_driver_name = dnf_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: def delete_race_guess(race_name: str, user_name: str) -> Response:
# Don't change guesses that are already over # 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): 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.query(DbRaceGuess).filter_by(race_name=race_name, user_name=user_name).delete()
db.session.commit() 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: 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. # 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: DbSeasonGuess = find_or_create_season_guess(user_name)
season_guess.hot_take = guesses[0] # type: ignore season_guess.hot_take = guesses[0] # type: ignore
season_guess.p2_team_name = guesses[1] # 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: 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 # Use strings as keys, as these dicts will be serialized to json
pxx_driver_names: Dict[str, str] = { pxx_driver_names: Dict[str, str] = {
str(position + 1): driver for position, driver in enumerate(pxx_driver_names_list) 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 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()))): 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 # First DNF drivers have to be contained in DNF drivers
for driver_name in first_dnf_driver_names_list: 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 # There can't be dnfs but no initial dnfs
if len(dnf_driver_names_list) > 0 and len(first_dnf_driver_names_list) == 0: 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: DbRaceResult = find_or_create_race_result(race_name)
race_result.pxx_driver_names_json = json.dumps(pxx_driver_names) 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: def update_user(user_name: str | None, add: bool = False, delete: bool = False) -> Response:
if user_name is None or len(user_name) < 3: if user_name is None:
return redirect("/user") return error_redirect("Invalid request: Cannot add/delete user because it is \"None\"!")
if not add and not delete: if not add and not delete:
return redirect("/user") return error_redirect("Invalid request: Can either add or delete user!")
if add and delete: if add and delete:
return redirect("/user") return error_redirect("Invalid request: Can either add or delete user!")
if add: 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): 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): elif user_exists_and_disabled(user_name):
disabled_user: DbUser | None = db.session.query(DbUser).filter_by(name=user_name, enabled=False).first() 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") return redirect("/user")
if delete: 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() enabled_user: DbUser | None = db.session.query(DbUser).filter_by(name=user_name, enabled=True).first()
if enabled_user is None: if enabled_user is None:
raise Exception("update_user couldn't disable user") 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 enabled_user.enabled = False
db.session.commit() db.session.commit()
else:
return error_redirect(f"User \"{user_name}\" was not deleted, because it does not exist!")
return redirect("/user") return redirect("/user")
raise Exception("update_user received illegal combination of arguments") raise Exception("update_user received illegal combination of arguments")

View File

@ -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") _T = TypeVar("_T")
@ -21,6 +26,28 @@ def positions_are_contiguous(positions: List[str]) -> bool:
# [2, 3, 4, 5]: 2 + 3 == 5 # [2, 3, 4, 5]: 2 + 3 == 5
return positions_sorted[0] + len(positions_sorted) - 1 == positions_sorted[-1] 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: def find_first_else_none(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T | None:
""" """

View File

@ -15,7 +15,7 @@ from formula10.frontend.model.race_result import RaceResult
from formula10.frontend.model.season_guess import SeasonGuess from formula10.frontend.model.season_guess import SeasonGuess
from formula10.frontend.model.team import NONE_TEAM, Team from formula10.frontend.model.team import NONE_TEAM, Team
from formula10.frontend.model.user import User 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 from formula10 import db
@ -35,6 +35,7 @@ class TemplateModel:
active_user: User | None = None active_user: User | None = None
active_result: RaceResult | 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"] _wdc_gained_excluded_abbrs: List[str] = ["RIC"]
def __init__(self, *, active_user_name: str | None, active_result_race_name: str | None): 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"]) self.active_user = self.user_by(user_name=active_user_name, ignore=["Everyone"])
if active_result_race_name is not None: 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: def active_user_name_or_everyone(self) -> str:
return self.active_user.name if self.active_user is not None else "Everyone" 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 return self.active_result.race.name
elif self.current_race is not None: elif self.current_race is not None:
return self.current_race.name 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: def active_result_race_name_or_current_race_name_sanitized(self) -> str:
if self.active_result is not None: if self.active_result is not None:
return self.active_result.race.name_sanitized return self.active_result.race.name_sanitized
elif self.current_race is not None: elif self.current_race is not None:
return self.current_race.name_sanitized 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]: def all_teams(self, *, include_none: bool) -> List[Team]:
""" """

View File

@ -189,7 +189,7 @@
<nav class="navbar fixed-top navbar-expand-lg bg-body-tertiary shadow-sm"> <nav class="navbar fixed-top navbar-expand-lg bg-body-tertiary shadow-sm">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/race"> <a class="navbar-brand" href="/race/Everyone">
<img src="../static/image/f1_logo.svg" alt="Logo" width="120" height="30" <img src="../static/image/f1_logo.svg" alt="Logo" width="120" height="30"
class="d-inline-block align-text-top"> class="d-inline-block align-text-top">
Formula 10 Formula 10

View File

@ -51,8 +51,7 @@
{{ model.active_result_race_name_or_current_race_name() }} {{ model.active_result_race_name_or_current_race_name() }}
</h5> </h5>
<form action="/result-enter/{{ model.active_result_race_name_or_current_race_name_sanitized() }}" <form action="/result-enter/{{ model.active_result_race_name_or_current_race_name_sanitized() }}" method="post">
method="post">
<ul id="columns" class="list-group list-group-flush"> <ul id="columns" class="list-group list-group-flush">
{% for driver in model.all_drivers_or_active_result_standing_drivers() %} {% for driver in model.all_drivers_or_active_result_standing_drivers() %}
@ -90,7 +89,8 @@
</div> </div>
{# Standing order #} {# Standing order #}
<input type="hidden" name="pxx-drivers" value="{{ driver.name }}"></li> <input type="hidden" name="pxx-drivers" value="{{ driver.name }}">
</li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -50,7 +50,8 @@
<td class="text-nowrap"> <td class="text-nowrap">
<span class="fw-bold">{{ model.current_race.number }}:</span> {{ model.current_race.name }}<br> <span class="fw-bold">{{ model.current_race.number }}:</span> {{ model.current_race.name }}<br>
<small><span class="fw-bold">Guess:</span> P{{ model.current_race.place_to_guess }}</small><br> <small><span class="fw-bold">Guess:</span> P{{ model.current_race.place_to_guess }}</small><br>
<small><span class="fw-bold">Date:</span> {{ model.current_race.date.strftime("%d.%m.%Y %H:%M") }}</small> <small><span class="fw-bold">Date:</span> {{ model.current_race.date.strftime("%d.%m.%Y %H:%M") }}
</small>
</td> </td>
{% if model.all_users() | length > 0 %} {% if model.all_users() | length > 0 %}
@ -85,12 +86,21 @@
<tr class="table-danger"> <tr class="table-danger">
<td class="text-nowrap"> <td class="text-nowrap">
<span class="fw-bold">{{ model.current_race.number }}:</span> {{ model.current_race.name }}<br> <span class="fw-bold">{{ model.current_race.number }}:</span> {{ model.current_race.name }}<br>
<small><span class="fw-bold">Guess:</span> P{{ model.current_race.place_to_guess }}</small> <small><span class="fw-bold">Guess:</span> P{{ model.current_race.place_to_guess }}</small><br>
<small><span class="fw-bold">Date:</span> {{ model.current_race.date.strftime("%d.%m.%Y %H:%M") }}
</td> </td>
<td> <td>
<form action="/race-guess/{{ model.current_race.name_sanitized }}/{{ model.active_user.name_sanitized }}" {% if model.race_guess_open(model.current_race) == true %}
method="post"> {% set action_save_href = "/race-guess/" ~ model.current_race.name_sanitized ~ "/" ~ model.active_user.name_sanitized %}
{% set action_delete_href = "/race-guess-delete/" ~ model.current_race.name_sanitized ~ "/" ~ model.active_user.name_sanitized %}
{% else %}
{% set action_save_href = "" %}
{% set action_delete_href = "" %}
{% endif %}
{# Enter + Save guess #}
<form action="{{ action_save_href }}" method="post">
{% set user_guess = model.race_guesses_by(user_name=model.active_user.name, race_name=model.current_race.name) %} {% set user_guess = model.race_guesses_by(user_name=model.active_user.name, race_name=model.current_race.name) %}
{# Driver PXX Select #} {# Driver PXX Select #}
@ -101,11 +111,12 @@
{# Driver DNF Select #} {# Driver DNF Select #}
{{ driver_select_with_preselect(driver_match=user_guess.dnf_guess, name="dnfselect", label="DNF:", include_none=true) }} {{ driver_select_with_preselect(driver_match=user_guess.dnf_guess, name="dnfselect", label="DNF:", include_none=true) }}
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save"> <input type="submit" class="btn btn-danger mt-2 w-100" value="Save" {% if model.race_guess_open(model.current_race) == false %}disabled="disabled"{% endif %}>
</form> </form>
<form action="/race-guess-delete/{{ model.current_race.name_sanitized }}/{{ model.active_user.name_sanitized }}"
method="post"> {# Delete guess #}
<input type="submit" class="btn btn-dark mt-2 w-100" value="Delete"> <form action="{{ action_delete_href }}" method="post">
<input type="submit" class="btn btn-dark mt-2 w-100" value="Delete" {% if model.race_guess_open(model.current_race) == false %}disabled{% endif %}>
</form> </form>
</td> </td>
@ -119,7 +130,8 @@
<td class="text-nowrap"> <td class="text-nowrap">
<span class="fw-bold">{{ past_result.race.number }}:</span> {{ past_result.race.name }}<br> <span class="fw-bold">{{ past_result.race.number }}:</span> {{ past_result.race.name }}<br>
<small><span class="fw-bold">Guessed:</span> P{{ past_result.race.place_to_guess }}</small><br> <small><span class="fw-bold">Guessed:</span> P{{ past_result.race.place_to_guess }}</small><br>
<small><span class="fw-bold">Date:</span> {{ past_result.race.date.strftime("%d.%m.%Y %H:%M") }}</small> <small><span class="fw-bold">Date:</span> {{ past_result.race.date.strftime("%d.%m.%Y %H:%M") }}
</small>
</td> </td>
{% if model.all_users_or_active_user() | length > 0 %} {% if model.all_users_or_active_user() | length > 0 %}

View File

@ -28,7 +28,12 @@
{% set user_guess = model.season_guesses_by(user_name=user.name) %} {% set user_guess = model.season_guesses_by(user_name=user.name) %}
<form action="/season-guess/{{ user.name }}" method="post"> {% if model.season_guess_open() == true %}
{% set action_save_href = "/season-guess" ~ user.name %}
{% else %}
{% set action_save_href = "" %}
{% endif %}
<form action="{{ action_save_href }}" method="post">
{# Hot Take #} {# Hot Take #}
<div class="form-floating"> <div class="form-floating">
@ -130,7 +135,7 @@
{% endfor %} {% endfor %}
</div> </div>
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save"> <input type="submit" class="btn btn-danger mt-2 w-100" value="Save" {% if model.season_guess_open() == false %}disabled{% endif %}>
</form> </form>
</div> </div>