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 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")

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")
@ -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:
"""

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.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]:
"""

View File

@ -189,7 +189,7 @@
<nav class="navbar fixed-top navbar-expand-lg bg-body-tertiary shadow-sm">
<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"
class="d-inline-block align-text-top">
Formula 10

View File

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

View File

@ -50,7 +50,8 @@
<td class="text-nowrap">
<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">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>
{% if model.all_users() | length > 0 %}
@ -85,12 +86,21 @@
<tr class="table-danger">
<td class="text-nowrap">
<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>
<form action="/race-guess/{{ model.current_race.name_sanitized }}/{{ model.active_user.name_sanitized }}"
method="post">
{% if model.race_guess_open(model.current_race) == true %}
{% 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) %}
{# Driver PXX Select #}
@ -101,11 +111,12 @@
{# Driver DNF Select #}
{{ 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 action="/race-guess-delete/{{ model.current_race.name_sanitized }}/{{ model.active_user.name_sanitized }}"
method="post">
<input type="submit" class="btn btn-dark mt-2 w-100" value="Delete">
{# Delete guess #}
<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>
</td>
@ -119,7 +130,8 @@
<td class="text-nowrap">
<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">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>
{% 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) %}
<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 #}
<div class="form-floating">
@ -130,7 +135,7 @@
{% endfor %}
</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>
</div>