Compare commits

...

10 Commits

Author SHA1 Message Date
b485791d25 Bug: Update method signature in single-user raceguess page
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 15s
2024-12-08 20:01:30 +01:00
e8c8e35e05 Disable info cards regarding season picks
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 11s
2024-12-08 19:57:24 +01:00
15305b2f3e Bug: Fix statistics issues caused by driver substitutions
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 16s
2024-12-08 19:52:02 +01:00
0dc4b22c72 Commented out leaderboard diagram extension for season points
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 16s
2024-12-08 19:38:31 +01:00
cef40a9e8b Implement season guess evaluation
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 15s
2024-12-08 19:25:06 +01:00
c509746688 Template: Mark wrong season guesses with red border
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 27s
2024-12-08 17:22:35 +01:00
9cdd7267db Bug: Fix missing inactive drivers in season guesses 2024-12-08 17:22:14 +01:00
61d247508f Bug: Fix duplicated place numbering in points model
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-10-19 14:01:46 +02:00
f38a5f2e6d Bug: Call delete_memoized with function references
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 33s
2024-10-19 13:41:14 +02:00
95760baebf Flake: Add sqlitebrowser 2024-10-19 13:40:51 +02:00
9 changed files with 451 additions and 187 deletions

View File

@ -111,6 +111,8 @@
packages = with pkgs; [ packages = with pkgs; [
myPython myPython
sqlitebrowser
nodejs_21 nodejs_21
nodePackages.sass nodePackages.sass
nodePackages.postcss-cli nodePackages.postcss-cli

View File

@ -29,7 +29,7 @@ cache.init_app(app)
# app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=("/formula10/*",), sort_by=("cumtime",)) # app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=("/formula10/*",), sort_by=("cumtime",))
# NOTE: These imports are required to register the routes. They need to be imported after "app" is declared # NOTE: These imports are required to register the routes. They need to be imported after "app" is declared
import formula10.controller.race_controller # type: ignore import formula10.controller.race_controller
import formula10.controller.season_controller import formula10.controller.season_controller
import formula10.controller.leaderboard_controller import formula10.controller.leaderboard_controller
import formula10.controller.statistics_controller import formula10.controller.statistics_controller

View File

@ -1,6 +1,8 @@
from typing import List from typing import Callable, List
from formula10 import cache from formula10 import cache
from formula10.domain.domain_model import Model
from formula10.domain.points_model import PointsModel
def cache_invalidate_user_updated() -> None: def cache_invalidate_user_updated() -> None:
@ -12,10 +14,10 @@ def cache_invalidate_user_updated() -> None:
"points_user_standing", "points_user_standing",
] ]
memoized_caches: List[str] = [ memoized_caches: List[Callable] = [
"points_by", PointsModel.points_by,
"race_guesses_by", PointsModel.race_guesses_by,
"season_guesses_by", PointsModel.season_guesses_by,
] ]
for c in caches: for c in caches:
@ -45,16 +47,16 @@ def cache_invalidate_race_result_updated() -> None:
"template_first_race_without_result", "template_first_race_without_result",
] ]
memoized_caches: List[str] = [ memoized_caches: List[Callable] = [
"driver_points_per_step", PointsModel.driver_points_per_step,
"driver_points_by", PointsModel.driver_points_by,
"total_driver_points_by", PointsModel.total_driver_points_by,
"drivers_sorted_by_points", PointsModel.drivers_sorted_by_points,
"total_team_points_by", PointsModel.total_team_points_by,
"points_by", PointsModel.points_by,
"is_team_winner", PointsModel.is_team_winner,
"has_podium", PointsModel.has_podium,
"picks_with_points_count", PointsModel.picks_with_points_count,
] ]
for c in caches: for c in caches:
@ -69,8 +71,8 @@ def cache_invalidate_race_guess_updated() -> None:
"domain_all_race_guesses", "domain_all_race_guesses",
] ]
memoized_caches: List[str] = [ memoized_caches: List[Callable] = [
"race_guesses_by", Model.race_guesses_by,
] ]
for c in caches: for c in caches:
@ -82,11 +84,11 @@ def cache_invalidate_race_guess_updated() -> None:
def cache_invalidate_season_guess_updated() -> None: def cache_invalidate_season_guess_updated() -> None:
caches: List[str] = [ caches: List[str] = [
"domain_all_season_guesses" "domain_all_season_guesses",
] ]
memoized_caches: List[str] = [ memoized_caches: List[Callable] = [
"season_guesses_by" Model.season_guesses_by,
] ]
for c in caches: for c in caches:

View File

@ -1,5 +1,5 @@
import json import json
from typing import Any, Callable, Dict, List, overload from typing import Any, Callable, Dict, List, overload, Tuple
import numpy as np import numpy as np
from formula10 import cache from formula10 import cache
@ -11,15 +11,11 @@ from formula10.domain.model.season_guess import SeasonGuess
from formula10.domain.model.season_guess_result import SeasonGuessResult from formula10.domain.model.season_guess_result import SeasonGuessResult
from formula10.domain.model.team import Team from formula10.domain.model.team import Team
from formula10.domain.model.user import User from formula10.domain.model.user import User
from formula10.database.validation import find_single_or_none_strict
# Guess points # Guess points
RACE_GUESS_OFFSET_POINTS: Dict[int, int] = { RACE_GUESS_OFFSET_POINTS: Dict[int, int] = {3: 1, 2: 3, 1: 6, 0: 10}
3: 1,
2: 3,
1: 6,
0: 10
}
RACE_GUESS_DNF_POINTS: int = 10 RACE_GUESS_DNF_POINTS: int = 10
SEASON_GUESS_HOT_TAKE_POINTS: int = 10 SEASON_GUESS_HOT_TAKE_POINTS: int = 10
SEASON_GUESS_P2_POINTS: int = 10 SEASON_GUESS_P2_POINTS: int = 10
@ -44,18 +40,9 @@ DRIVER_RACE_POINTS: Dict[int, int] = {
7: 6, 7: 6,
8: 4, 8: 4,
9: 2, 9: 2,
10: 1 10: 1,
}
DRIVER_SPRINT_POINTS: Dict[int, int] = {
1: 8,
2: 7,
3: 6,
4: 5,
5: 4,
6: 3,
7: 2,
8: 1
} }
DRIVER_SPRINT_POINTS: Dict[int, int] = {1: 8, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1}
DRIVER_FASTEST_LAP_POINTS: int = 1 DRIVER_FASTEST_LAP_POINTS: int = 1
# Last season results # Last season results
@ -80,8 +67,9 @@ WDC_STANDING_2023: Dict[str, int] = {
"Daniel Ricciardo": 17, "Daniel Ricciardo": 17,
"Zhou Guanyu": 18, "Zhou Guanyu": 18,
"Kevin Magnussen": 19, "Kevin Magnussen": 19,
"Logan Sargeant": 21 "Logan Sargeant": 21,
} }
WCC_STANDING_2023: Dict[str, int] = { WCC_STANDING_2023: Dict[str, int] = {
"Red Bull": 1, "Red Bull": 1,
"Mercedes": 2, "Mercedes": 2,
@ -92,11 +80,47 @@ WCC_STANDING_2023: Dict[str, int] = {
"Williams": 7, "Williams": 7,
"VCARB": 8, "VCARB": 8,
"Sauber": 9, "Sauber": 9,
"Haas": 10 "Haas": 10,
}
# In case a substitute driver is driving, those points have to be subtracted from the actual driver
# (Driver_ID, Race_ID, Points)
WDC_SUBSTITUTE_POINTS: List[Tuple[int, int, int]] = [
(15, 2, 6), # Bearman raced for Sainz in Saudi Arabia
(8, 17, 1), # Bearman raced for Magnussen in Azerbaijan
]
WDC_STANDING_2024: Dict[str, int] = {
"Max Verstappen": 1,
"Lando Norris": 2,
"Charles Leclerc": 3,
"Oscar Piastri": 4,
"Carlos Sainz": 5,
"George Russell": 6,
"Lewis Hamilton": 7,
"Sergio Perez": 8,
"Fernando Alonso": 9,
"Pierre Gasly": 10,
"Nico Hulkenberg": 11,
"Yuki Tsunoda": 12,
"Lance Stroll": 13,
"Esteban Ocon": 14,
"Kevin Magnussen": 15,
"Alexander Albon": 16,
"Daniel Ricciardo": 17,
"Oliver Bearman": 18,
"Franco Colapinto": 19,
"Zhou Guanyu": 20,
"Liam Lawson": 21,
"Valtteri Bottas": 22,
"Logan Sargeant": 23,
"Jack Doohan": 24
} }
def standing_points(race_guess: RaceGuess, race_result: RaceResult) -> int: def standing_points(race_guess: RaceGuess, race_result: RaceResult) -> int:
guessed_driver_position: int | None = race_result.driver_standing_position(driver=race_guess.pxx_guess) guessed_driver_position: int | None = race_result.driver_standing_position(
driver=race_guess.pxx_guess
)
if guessed_driver_position is None: if guessed_driver_position is None:
return 0 return 0
@ -106,6 +130,7 @@ def standing_points(race_guess: RaceGuess, race_result: RaceResult) -> int:
return RACE_GUESS_OFFSET_POINTS[position_offset] return RACE_GUESS_OFFSET_POINTS[position_offset]
def dnf_points(race_guess: RaceGuess, race_result: RaceResult) -> int: def dnf_points(race_guess: RaceGuess, race_result: RaceResult) -> int:
if race_guess.dnf_guess in race_result.initial_dnf: if race_guess.dnf_guess in race_result.initial_dnf:
return RACE_GUESS_DNF_POINTS return RACE_GUESS_DNF_POINTS
@ -115,6 +140,17 @@ def dnf_points(race_guess: RaceGuess, race_result: RaceResult) -> int:
return 0 return 0
def substitute_points(driver: Driver, race_number: int) -> int:
predicate: Callable[[Tuple[int, int, int]], bool] = lambda substitution: driver.id == substitution[0] and race_number == substitution[1]
substitution: Tuple[int, int, int] = find_single_or_none_strict(predicate, WDC_SUBSTITUTE_POINTS)
if substitution is not None:
return substitution[2]
else:
return 0
class PointsModel(Model): class PointsModel(Model):
""" """
This class bundles all data + functionality required to do points calculations. This class bundles all data + functionality required to do points calculations.
@ -123,51 +159,78 @@ class PointsModel(Model):
def __init__(self): def __init__(self):
Model.__init__(self) Model.__init__(self)
@cache.cached(timeout=None, key_prefix="points_points_per_step") # Clear when adding/updating race results or users @cache.cached(
timeout=None, key_prefix="points_points_per_step"
) # Clear when adding/updating race results or users
def points_per_step(self) -> Dict[str, List[int]]: def points_per_step(self) -> Dict[str, List[int]]:
""" """
Returns a dictionary of lists, containing points per race for each user. Returns a dictionary of lists, containing points per race for each user.
""" """
points_per_step = dict() points_per_step = dict()
for user in self.all_users(): for user in self.all_users():
points_per_step[user.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers points_per_step[user.name] = [0] * (
len(self.all_races()) + 1
) # Start at index 1, like the race numbers
for race_guess in self.all_race_guesses(): for race_guess in self.all_race_guesses():
user_name: str = race_guess.user.name user_name: str = race_guess.user.name
race_number: int = race_guess.race.number race_number: int = race_guess.race.number
race_result: RaceResult | None = self.race_result_by(race_name=race_guess.race.name) race_result: RaceResult | None = self.race_result_by(
race_name=race_guess.race.name
)
if race_result is None: if race_result is None:
continue continue
points_per_step[user_name][race_number] = standing_points(race_guess, race_result) + dnf_points(race_guess, race_result) points_per_step[user_name][race_number] = standing_points(
race_guess, race_result
) + dnf_points(race_guess, race_result)
return points_per_step return points_per_step
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Clear when adding/updating race results @cache.memoize(
timeout=None, args_to_ignore=["self"]
) # Clear when adding/updating race results
def driver_points_per_step(self, *, include_inactive: bool) -> Dict[str, List[int]]: def driver_points_per_step(self, *, include_inactive: bool) -> Dict[str, List[int]]:
""" """
Returns a dictionary of lists, containing points per race for each driver. Returns a dictionary of lists, containing points per race for each driver.
""" """
driver_points_per_step = dict() driver_points_per_step = dict()
for driver in self.all_drivers(include_none=False, include_inactive=include_inactive): for driver in self.all_drivers(
driver_points_per_step[driver.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers include_none=False, include_inactive=include_inactive
):
driver_points_per_step[driver.name] = [0] * (
len(self.all_races()) + 1
) # Start at index 1, like the race numbers
for race_result in self.all_race_results(): for race_result in self.all_race_results():
race_number: int = race_result.race.number race_number: int = race_result.race.number
for position, driver in race_result.standing.items(): for position, driver in race_result.standing.items():
driver_points_per_step[driver.name][race_number] = DRIVER_RACE_POINTS[int(position)] if int(position) in DRIVER_RACE_POINTS else 0 driver_points_per_step[driver.name][race_number] = (
driver_points_per_step[driver.name][race_number] += DRIVER_FASTEST_LAP_POINTS if race_result.fastest_lap_driver == driver else 0 DRIVER_RACE_POINTS[int(position)]
if int(position) in DRIVER_RACE_POINTS
else 0
)
driver_points_per_step[driver.name][race_number] += (
DRIVER_FASTEST_LAP_POINTS
if race_result.fastest_lap_driver == driver
and int(position) <= 10
else 0
)
driver_points_per_step[driver.name][race_number] -= substitute_points(driver, race_number)
for position, driver in race_result.sprint_standing.items(): for position, driver in race_result.sprint_standing.items():
driver_name: str = driver.name driver_name: str = driver.name
driver_points_per_step[driver_name][race_number] += DRIVER_SPRINT_POINTS[int(position)] if int(position) in DRIVER_SPRINT_POINTS else 0 driver_points_per_step[driver_name][race_number] += (
DRIVER_SPRINT_POINTS[int(position)]
if int(position) in DRIVER_SPRINT_POINTS
else 0
)
return driver_points_per_step return driver_points_per_step
@cache.cached(timeout=None, key_prefix="points_team_points_per_step") @cache.cached(timeout=None, key_prefix="points_team_points_per_step")
def team_points_per_step(self) -> Dict[str, List[int]]: def team_points_per_step(self) -> Dict[str, List[int]]:
""" """
@ -175,14 +238,20 @@ class PointsModel(Model):
""" """
team_points_per_step = dict() team_points_per_step = dict()
for team in self.all_teams(include_none=False): for team in self.all_teams(include_none=False):
team_points_per_step[team.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers team_points_per_step[team.name] = [0] * (
len(self.all_races()) + 1
) # Start at index 1, like the race numbers
for race_result in self.all_race_results(): for race_result in self.all_race_results():
for driver in race_result.standing.values(): for driver in race_result.standing.values():
team_name: str = driver.team.name team_name: str = driver.team.name
race_number: int = race_result.race.number race_number: int = race_result.race.number
team_points_per_step[team_name][race_number] += self.driver_points_per_step(include_inactive=True)[driver.name][race_number] team_points_per_step[team_name][
race_number
] += self.driver_points_per_step(include_inactive=True)[driver.name][
race_number
]
return team_points_per_step return team_points_per_step
@ -206,48 +275,78 @@ class PointsModel(Model):
# Driver stats # Driver stats
# #
@cache.cached(timeout=None, key_prefix="points_driver_points_per_step_cumulative") # Cleanup when adding/updating race results @cache.cached(
timeout=None, key_prefix="points_driver_points_per_step_cumulative"
) # Cleanup when adding/updating race results
def driver_points_per_step_cumulative(self) -> Dict[str, List[int]]: def driver_points_per_step_cumulative(self) -> Dict[str, List[int]]:
""" """
Returns a dictionary of lists, containing cumulative points per race for each driver. Returns a dictionary of lists, containing cumulative points per race for each driver.
""" """
points_per_step_cumulative: Dict[str, List[int]] = dict() points_per_step_cumulative: Dict[str, List[int]] = dict()
for driver_name, points in self.driver_points_per_step(include_inactive=True).items(): for driver_name, points in self.driver_points_per_step(
include_inactive=True
).items():
points_per_step_cumulative[driver_name] = np.cumsum(points).tolist() points_per_step_cumulative[driver_name] = np.cumsum(points).tolist()
return points_per_step_cumulative return points_per_step_cumulative
@overload @overload
def driver_points_by(self, *, driver_name: str, include_inactive: bool) -> List[int]: def driver_points_by(
self, *, driver_name: str, include_inactive: bool
) -> List[int]:
""" """
Returns a list of points per race for a specific driver. Returns a list of points per race for a specific driver.
""" """
return self.driver_points_by(driver_name=driver_name, include_inactive=include_inactive) return self.driver_points_by(
driver_name=driver_name, include_inactive=include_inactive
)
@overload @overload
def driver_points_by(self, *, race_name: str, include_inactive: bool) -> Dict[str, int]: def driver_points_by(
self, *, race_name: str, include_inactive: bool
) -> Dict[str, int]:
""" """
Returns a dictionary of points per driver for a specific race. Returns a dictionary of points per driver for a specific race.
""" """
return self.driver_points_by(race_name=race_name, include_inactive=include_inactive) return self.driver_points_by(
race_name=race_name, include_inactive=include_inactive
)
@overload @overload
def driver_points_by(self, *, driver_name: str, race_name: str, include_inactive: bool) -> int: def driver_points_by(
self, *, driver_name: str, race_name: str, include_inactive: bool
) -> int:
""" """
Returns the points for a specific race for a specific driver. Returns the points for a specific race for a specific driver.
""" """
return self.driver_points_by(driver_name=driver_name, race_name=race_name, include_inactive=include_inactive) return self.driver_points_by(
driver_name=driver_name,
race_name=race_name,
include_inactive=include_inactive,
)
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results @cache.memoize(
def driver_points_by(self, *, driver_name: str | None = None, race_name: str | None = None, include_inactive: bool) -> List[int] | Dict[str, int] | int: timeout=None, args_to_ignore=["self"]
) # Cleanup when adding/updating race results
def driver_points_by(
self,
*,
driver_name: str | None = None,
race_name: str | None = None,
include_inactive: bool
) -> List[int] | Dict[str, int] | int:
if driver_name is not None and race_name is None: if driver_name is not None and race_name is None:
return self.driver_points_per_step(include_inactive=include_inactive)[driver_name] return self.driver_points_per_step(include_inactive=include_inactive)[
driver_name
]
if driver_name is None and race_name is not None: if driver_name is None and race_name is not None:
race_number: int = self.race_by(race_name=race_name).number race_number: int = self.race_by(race_name=race_name).number
points_by_race: Dict[str, int] = dict() points_by_race: Dict[str, int] = dict()
for _driver_name, points in self.driver_points_per_step(include_inactive=include_inactive).items(): for _driver_name, points in self.driver_points_per_step(
include_inactive=include_inactive
).items():
points_by_race[_driver_name] = points[race_number] points_by_race[_driver_name] = points[race_number]
return points_by_race return points_by_race
@ -255,63 +354,108 @@ class PointsModel(Model):
if driver_name is not None and race_name is not None: if driver_name is not None and race_name is not None:
race_number: int = self.race_by(race_name=race_name).number race_number: int = self.race_by(race_name=race_name).number
return self.driver_points_per_step(include_inactive=include_inactive)[driver_name][race_number] return self.driver_points_per_step(include_inactive=include_inactive)[
driver_name
][race_number]
raise Exception("driver_points_by received an illegal combination of arguments") raise Exception("driver_points_by received an illegal combination of arguments")
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results @cache.memoize(
timeout=None, args_to_ignore=["self"]
) # Cleanup when adding/updating race results
def total_driver_points_by(self, driver_name: str) -> int: def total_driver_points_by(self, driver_name: str) -> int:
return sum(self.driver_points_by(driver_name=driver_name, include_inactive=True)) return sum(
self.driver_points_by(driver_name=driver_name, include_inactive=True)
)
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results @cache.memoize(
timeout=None, args_to_ignore=["self"]
) # Cleanup when adding/updating race results
def drivers_sorted_by_points(self, *, include_inactive: bool) -> List[Driver]: def drivers_sorted_by_points(self, *, include_inactive: bool) -> List[Driver]:
comparator: Callable[[Driver], int] = lambda driver: self.total_driver_points_by(driver.name) comparator: Callable[[Driver], int] = (
return sorted(self.all_drivers(include_none=False, include_inactive=include_inactive), key=comparator, reverse=True) lambda driver: self.total_driver_points_by(driver.name)
)
return sorted(
self.all_drivers(include_none=False, include_inactive=include_inactive),
key=comparator,
reverse=True,
)
@cache.cached(timeout=None, key_prefix="points_wdc_standing_by_position") # Cleanup when adding/updating race results @cache.cached(
timeout=None, key_prefix="points_wdc_standing_by_position"
) # Cleanup when adding/updating race results
def wdc_standing_by_position(self) -> Dict[int, List[str]]: def wdc_standing_by_position(self) -> Dict[int, List[str]]:
standing: Dict[int, List[str]] = dict() standing: Dict[int, List[str]] = dict()
for position in range(1, len(self.all_drivers(include_none=False, include_inactive=True)) + 1): if WDC_STANDING_2024 is None:
standing[position] = list() for position in range(
1, len(self.all_drivers(include_none=False, include_inactive=True)) + 1
):
standing[position] = list()
position: int = 1 position: int = 1
last_points: int = 0 last_points: int = 0
for driver in self.drivers_sorted_by_points(include_inactive=True): for driver in self.drivers_sorted_by_points(include_inactive=True):
points: int = self.total_driver_points_by(driver.name) points: int = self.total_driver_points_by(driver.name)
if points < last_points: if points < last_points:
position += 1 # If multiple drivers have equal points, a place is shared.
# In this case, the next driver does not occupy the immediate next position.
position += len(standing[position])
standing[position].append(driver.name) standing[position].append(driver.name)
last_points = points last_points = points
if WDC_STANDING_2024 is not None:
for position in range(1, len(WDC_STANDING_2024) + 1):
standing[position] = list()
for driver, position in WDC_STANDING_2024.items():
standing[position] += [driver]
return standing return standing
@cache.cached(timeout=None, key_prefix="points_wdc_standing_by_driver") # Cleanup when adding/updating race results @cache.cached(
timeout=None, key_prefix="points_wdc_standing_by_driver"
) # Cleanup when adding/updating race results
def wdc_standing_by_driver(self) -> Dict[str, int]: def wdc_standing_by_driver(self) -> Dict[str, int]:
standing: Dict[str, int] = dict() standing: Dict[str, int] = dict()
position: int = 1 if WDC_STANDING_2024 is None:
last_points: int = 0 position: int = 1
last_points: int = 0
for driver in self.drivers_sorted_by_points(include_inactive=True): for driver in self.drivers_sorted_by_points(include_inactive=True):
points: int = self.total_driver_points_by(driver.name) points: int = self.total_driver_points_by(driver.name)
if points < last_points: if points < last_points:
position += 1 drivers_with_this_position = 0
for _driver, _position in standing.items():
if _position == position:
drivers_with_this_position += 1
standing[driver.name] = position # If multiple drivers have equal points, a place is shared.
last_points = points # In this case, the next driver does not occupy the immediate next position.
position += drivers_with_this_position
return standing standing[driver.name] = position
last_points = points
return standing
if WDC_STANDING_2024 is not None:
return WDC_STANDING_2024
def wdc_diff_2023_by(self, driver_name: str) -> int: def wdc_diff_2023_by(self, driver_name: str) -> int:
if not driver_name in WDC_STANDING_2023: if not driver_name in WDC_STANDING_2023:
return 0 return 0
return WDC_STANDING_2023[driver_name] - self.wdc_standing_by_driver()[driver_name] return (
WDC_STANDING_2023[driver_name] - self.wdc_standing_by_driver()[driver_name]
)
@cache.cached(timeout=None, key_prefix="points_most_dnf_names") # Cleanup when adding/updating race results @cache.cached(
timeout=None, key_prefix="points_most_dnf_names"
) # Cleanup when adding/updating race results
def most_dnf_names(self) -> List[str]: def most_dnf_names(self) -> List[str]:
dnf_names: List[str] = list() dnf_names: List[str] = list()
most_dnfs: int = 0 most_dnfs: int = 0
@ -326,7 +470,9 @@ class PointsModel(Model):
return dnf_names return dnf_names
@cache.cached(timeout=None, key_prefix="points_most_gained_names") # Cleanup when adding/updating race results @cache.cached(
timeout=None, key_prefix="points_most_gained_names"
) # Cleanup when adding/updating race results
def most_gained_names(self) -> List[str]: def most_gained_names(self) -> List[str]:
most_gained_names: List[str] = list() most_gained_names: List[str] = list()
most_gained: int = 0 most_gained: int = 0
@ -345,7 +491,9 @@ class PointsModel(Model):
return most_gained_names return most_gained_names
@cache.cached(timeout=None, key_prefix="points_most_lost_names") # Cleanup when adding/updating race results @cache.cached(
timeout=None, key_prefix="points_most_lost_names"
) # Cleanup when adding/updating race results
def most_lost_names(self) -> List[str]: def most_lost_names(self) -> List[str]:
most_lost_names: List[str] = list() most_lost_names: List[str] = list()
most_lost: int = 100 most_lost: int = 100
@ -368,7 +516,9 @@ class PointsModel(Model):
# Team points # Team points
# #
@cache.cached(timeout=None, key_prefix="points_team_points_per_step_cumulative") # Cleanup when adding/updating race results @cache.cached(
timeout=None, key_prefix="points_team_points_per_step_cumulative"
) # Cleanup when adding/updating race results
def team_points_per_step_cumulative(self) -> Dict[str, List[int]]: def team_points_per_step_cumulative(self) -> Dict[str, List[int]]:
""" """
Returns a dictionary of lists, containing cumulative points per race for each team. Returns a dictionary of lists, containing cumulative points per race for each team.
@ -379,21 +529,34 @@ class PointsModel(Model):
return points_per_step_cumulative return points_per_step_cumulative
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results @cache.memoize(
timeout=None, args_to_ignore=["self"]
) # Cleanup when adding/updating race results
def total_team_points_by(self, team_name: str) -> int: def total_team_points_by(self, team_name: str) -> int:
teammates: List[Driver] = self.drivers_by(team_name=team_name, include_inactive=True) teammates: List[Driver] = self.drivers_by(
return sum(sum(self.driver_points_by(driver_name=teammate.name, include_inactive=True)) for teammate in teammates) team_name=team_name, include_inactive=True
)
return sum(
sum(self.driver_points_by(driver_name=teammate.name, include_inactive=True))
for teammate in teammates
)
@cache.cached(timeout=None, key_prefix="points_teams_sorted_by_points") # Cleanup when adding/updating race results @cache.cached(
timeout=None, key_prefix="points_teams_sorted_by_points"
) # Cleanup when adding/updating race results
def teams_sorted_by_points(self) -> List[Team]: def teams_sorted_by_points(self) -> List[Team]:
comparator: Callable[[Team], int] = lambda team: self.total_team_points_by(team.name) comparator: Callable[[Team], int] = lambda team: self.total_team_points_by(
team.name
)
return sorted(self.all_teams(include_none=False), key=comparator, reverse=True) return sorted(self.all_teams(include_none=False), key=comparator, reverse=True)
@cache.cached(timeout=None, key_prefix="points_wcc_standing_by_position") # Cleanup when adding/updating race results @cache.cached(
timeout=None, key_prefix="points_wcc_standing_by_position"
) # Cleanup when adding/updating race results
def wcc_standing_by_position(self) -> Dict[int, List[str]]: def wcc_standing_by_position(self) -> Dict[int, List[str]]:
standing: Dict[int, List[str]] = dict() standing: Dict[int, List[str]] = dict()
for position in range (1, len(self.all_teams(include_none=False)) + 1): for position in range(1, len(self.all_teams(include_none=False)) + 1):
standing[position] = list() standing[position] = list()
position: int = 1 position: int = 1
@ -402,14 +565,18 @@ class PointsModel(Model):
for team in self.teams_sorted_by_points(): for team in self.teams_sorted_by_points():
points: int = self.total_team_points_by(team.name) points: int = self.total_team_points_by(team.name)
if points < last_points: if points < last_points:
position += 1 # If multiple teams have equal points, a place is shared.
# In this case, the next team does not occupy the immediate next position.
position += len(standing[position])
standing[position].append(team.name) standing[position].append(team.name)
last_points = points last_points = points
return standing return standing
@cache.cached(timeout=None, key_prefix="points_wcc_standing_by_team") # Cleanup when adding/updating race results @cache.cached(
timeout=None, key_prefix="points_wcc_standing_by_team"
) # Cleanup when adding/updating race results
def wcc_standing_by_team(self) -> Dict[str, int]: def wcc_standing_by_team(self) -> Dict[str, int]:
standing: Dict[str, int] = dict() standing: Dict[str, int] = dict()
@ -419,7 +586,14 @@ class PointsModel(Model):
for team in self.teams_sorted_by_points(): for team in self.teams_sorted_by_points():
points: int = self.total_team_points_by(team.name) points: int = self.total_team_points_by(team.name)
if points < last_points: if points < last_points:
position += 1 teams_with_this_position = 0
for _team, _position in standing.items():
if _position == position:
teams_with_this_position += 1
# If multiple teams have equal points, a place is shared.
# In this case, the next team does not occupy the immediate next position.
position += teams_with_this_position
standing[team.name] = position standing[team.name] = position
last_points = points last_points = points
@ -464,8 +638,12 @@ class PointsModel(Model):
""" """
return self.points_by(user_name=user_name, race_name=race_name) return self.points_by(user_name=user_name, race_name=race_name)
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results or users @cache.memoize(
def points_by(self, *, user_name: str | None = None, race_name: str | None = None) -> List[int] | Dict[str, int] | int: timeout=None, args_to_ignore=["self"]
) # Cleanup when adding/updating race results or users
def points_by(
self, *, user_name: str | None = None, race_name: str | None = None
) -> List[int] | Dict[str, int] | int:
if user_name is not None and race_name is None: if user_name is not None and race_name is None:
return self.points_per_step()[user_name] return self.points_per_step()[user_name]
@ -485,32 +663,76 @@ class PointsModel(Model):
raise Exception("points_by received an illegal combination of arguments") raise Exception("points_by received an illegal combination of arguments")
def total_points_by(self, user_name: str) -> int: def season_points_by(self, *, user_name: str) -> int:
"""
Returns the number of points from seasonguesses for a specific user.
"""
big_picks = (int(self.hot_take_correct(user_name=user_name)) * 10
+ int(self.p2_constructor_correct(user_name=user_name)) * 10
+ int(self.overtakes_correct(user_name=user_name)) * 10
+ int(self.dnfs_correct(user_name=user_name)) * 10
+ int(self.most_gained_correct(user_name=user_name)) * 10
+ int(self.most_lost_correct(user_name=user_name)) * 10)
small_picks = 0
guess: SeasonGuess = self.season_guesses_by(user_name=user_name)
for driver in guess.team_winners:
if self.is_team_winner(driver):
small_picks += 3
else:
small_picks -= 3
# NOTE: Not picked drivers that had a podium are also wrong
for driver in self.all_drivers(include_none=False, include_inactive=True):
if driver in guess.podiums and self.has_podium(driver):
small_picks += 3
elif driver in guess.podiums and not self.has_podium(driver):
small_picks -=2
elif driver not in guess.podiums and self.has_podium(driver):
small_picks -=2
return big_picks + small_picks
def total_points_by(self, *, user_name: str, include_season: bool) -> int:
""" """
Returns the total number of points for a specific user. Returns the total number of points for a specific user.
""" """
return sum(self.points_by(user_name=user_name)) if include_season:
return sum(self.points_by(user_name=user_name)) + self.season_points_by(user_name=user_name)
else:
return sum(self.points_by(user_name=user_name))
def users_sorted_by_points(self) -> List[User]: def users_sorted_by_points(self, *, include_season: bool) -> List[User]:
""" """
Returns the list of users, sorted by their points from race guesses (in descending order). Returns the list of users, sorted by their points from race guesses (in descending order).
""" """
comparator: Callable[[User], int] = lambda user: self.total_points_by(user.name) comparator: Callable[[User], int] = lambda user: self.total_points_by(user_name=user.name, include_season=include_season)
return sorted(self.all_users(), key=comparator, reverse=True) return sorted(self.all_users(), key=comparator, reverse=True)
@cache.cached(timeout=None, key_prefix="points_user_standing") # Cleanup when adding/updating race results or users @cache.cached(
def user_standing(self) -> Dict[str, int]: timeout=None, key_prefix="points_user_standing"
) # Cleanup when adding/updating race results or users
def user_standing(self, *, include_season: bool) -> Dict[str, int]:
standing: Dict[str, int] = dict() standing: Dict[str, int] = dict()
position: int = 1 position: int = 1
last_points: int = 0 last_points: int = 0
for user in self.users_sorted_by_points():
if self.total_points_by(user.name) < last_points: for user in self.users_sorted_by_points(include_season=include_season):
position += 1 if self.total_points_by(user_name=user.name, include_season=include_season) < last_points:
users_with_this_position = 0
for _user, _position in standing.items():
if _position == position:
users_with_this_position += 1
# If multiple users have equal points, a place is shared.
# In this case, the next user does not occupy the immediate next position.
position += users_with_this_position
standing[user.name] = position standing[user.name] = position
last_points = self.total_points_by(user.name) last_points = self.total_points_by(user_name=user.name, include_season=include_season)
return standing return standing
@ -518,12 +740,16 @@ class PointsModel(Model):
# Treat standing + dnf picks separately # Treat standing + dnf picks separately
return len(self.race_guesses_by(user_name=user_name)) * 2 return len(self.race_guesses_by(user_name=user_name)) * 2
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results @cache.memoize(
timeout=None, args_to_ignore=["self"]
) # Cleanup when adding/updating race results
def picks_with_points_count(self, user_name: str) -> int: def picks_with_points_count(self, user_name: str) -> int:
count: int = 0 count: int = 0
for race_guess in self.race_guesses_by(user_name=user_name): for race_guess in self.race_guesses_by(user_name=user_name):
race_result: RaceResult | None = self.race_result_by(race_name=race_guess.race.name) race_result: RaceResult | None = self.race_result_by(
race_name=race_guess.race.name
)
if race_result is None: if race_result is None:
continue continue
@ -538,16 +764,22 @@ class PointsModel(Model):
if self.picks_count(user_name) == 0: if self.picks_count(user_name) == 0:
return 0.0 return 0.0
return self.total_points_by(user_name) / self.picks_count(user_name) return self.total_points_by(user_name=user_name, include_season=False) / self.picks_count(user_name)
# #
# Season guess evaluation # Season guess evaluation
# #
def hot_take_correct(self, user_name: str) -> bool: def hot_take_correct(self, user_name: str) -> bool:
season_guess_result: SeasonGuessResult | None = self.season_guess_result_by(user_name=user_name) season_guess_result: SeasonGuessResult | None = self.season_guess_result_by(
user_name=user_name
)
return season_guess_result.hot_take_correct if season_guess_result is not None else False return (
season_guess_result.hot_take_correct
if season_guess_result is not None
else False
)
def p2_constructor_correct(self, user_name: str) -> bool: def p2_constructor_correct(self, user_name: str) -> bool:
season_guess: SeasonGuess | None = self.season_guesses_by(user_name=user_name) season_guess: SeasonGuess | None = self.season_guesses_by(user_name=user_name)
@ -558,9 +790,15 @@ class PointsModel(Model):
return season_guess.p2_wcc.name in self.wcc_standing_by_position()[2] return season_guess.p2_wcc.name in self.wcc_standing_by_position()[2]
def overtakes_correct(self, user_name: str) -> bool: def overtakes_correct(self, user_name: str) -> bool:
season_guess_result: SeasonGuessResult | None = self.season_guess_result_by(user_name=user_name) season_guess_result: SeasonGuessResult | None = self.season_guess_result_by(
user_name=user_name
)
return season_guess_result.overtakes_correct if season_guess_result is not None else False return (
season_guess_result.overtakes_correct
if season_guess_result is not None
else False
)
def dnfs_correct(self, user_name: str) -> bool: def dnfs_correct(self, user_name: str) -> bool:
season_guess: SeasonGuess | None = self.season_guesses_by(user_name=user_name) season_guess: SeasonGuess | None = self.season_guesses_by(user_name=user_name)
@ -586,14 +824,22 @@ class PointsModel(Model):
return season_guess.most_wdc_lost.name in self.most_lost_names() return season_guess.most_wdc_lost.name in self.most_lost_names()
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results @cache.memoize(
timeout=None, args_to_ignore=["self"]
) # Cleanup when adding/updating race results
def is_team_winner(self, driver: Driver) -> bool: def is_team_winner(self, driver: Driver) -> bool:
teammates: List[Driver] = self.drivers_by(team_name=driver.team.name, include_inactive=True) teammates: List[Driver] = self.drivers_by(
teammate: Driver = teammates[0] if teammates[1] == driver else teammates[1] team_name=driver.team.name, include_inactive=True
)
return self.wdc_standing_by_driver()[driver.name] <= self.wdc_standing_by_driver()[teammate.name] # Min - Highest position is the lowest place number
winner: Driver = min(teammates, key=lambda driver: self.wdc_standing_by_driver()[driver.name])
@cache.memoize(timeout=None, args_to_ignore=["self"]) # Cleanup when adding/updating race results return driver == winner
@cache.memoize(
timeout=None, args_to_ignore=["self"]
) # Cleanup when adding/updating race results
def has_podium(self, driver: Driver) -> bool: def has_podium(self, driver: Driver) -> bool:
for race_result in self.all_race_results(): for race_result in self.all_race_results():
position: int | None = race_result.driver_standing_position(driver) position: int | None = race_result.driver_standing_position(driver)
@ -611,13 +857,13 @@ class PointsModel(Model):
data["labels"] = [0] + [ data["labels"] = [0] + [
race.name for race in sorted(self.all_races(), key=lambda race: race.number) race.name for race in sorted(self.all_races(), key=lambda race: race.number)
] ] # + ["Season"]
data["datasets"] = [ data["datasets"] = [
{ {
"data": self.points_per_step_cumulative()[user.name], "data": self.points_per_step_cumulative()[user.name], # + [self.total_points_by(user_name=user.name, include_season=True)],
"label": user.name, "label": user.name,
"fill": False "fill": False,
} }
for user in self.all_users() for user in self.all_users()
] ]
@ -635,7 +881,7 @@ class PointsModel(Model):
{ {
"data": self.driver_points_per_step_cumulative()[driver.name], "data": self.driver_points_per_step_cumulative()[driver.name],
"label": driver.abbr, "label": driver.abbr,
"fill": False "fill": False,
} }
for driver in self.all_drivers(include_none=False, include_inactive=True) for driver in self.all_drivers(include_none=False, include_inactive=True)
] ]
@ -653,7 +899,7 @@ class PointsModel(Model):
{ {
"data": self.team_points_per_step_cumulative()[team.name], "data": self.team_points_per_step_cumulative()[team.name],
"label": team.name, "label": team.name,
"fill": False "fill": False,
} }
for team in self.all_teams(include_none=False) for team in self.all_teams(include_none=False)
] ]

View File

@ -104,4 +104,4 @@ class TemplateModel(Model):
def active_drivers_for_wdc_gained(self) -> List[Driver]: def active_drivers_for_wdc_gained(self) -> List[Driver]:
predicate: Callable[[Driver], bool] = lambda driver: driver.abbr not in self._wdc_gained_excluded_abbrs predicate: Callable[[Driver], bool] = lambda driver: driver.abbr not in self._wdc_gained_excluded_abbrs
return find_multiple_strict(predicate, self.all_drivers(include_none=False, include_inactive=False)) return find_multiple_strict(predicate, self.all_drivers(include_none=False, include_inactive=True))

View File

@ -24,13 +24,13 @@
{% endmacro %} {% endmacro %}
{# Simple driver select for forms #} {# Simple driver select for forms #}
{% macro driver_select(name, label, include_none, drivers=none, disabled=false, border="") %} {% macro driver_select(name, label, include_none, include_inactive=false, drivers=none, disabled=false, border="") %}
<div class="form-floating"> <div class="form-floating">
<select name="{{ name }}" id="{{ name }}" class="form-select {{ border }}" aria-label="{{ name }}" {% if disabled %}disabled="disabled"{% endif %}> <select name="{{ name }}" id="{{ name }}" class="form-select {{ border }}" aria-label="{{ name }}" {% if disabled %}disabled="disabled"{% endif %}>
<option value="" selected disabled hidden></option> <option value="" selected disabled hidden></option>
{% if drivers == none %} {% if drivers == none %}
{% set drivers = model.all_drivers(include_none=include_none, include_inactive=False) %} {% set drivers = model.all_drivers(include_none=include_none, include_inactive=include_inactive) %}
{% endif %} {% endif %}
{% for driver in drivers %} {% for driver in drivers %}
@ -42,14 +42,14 @@
{% endmacro %} {% endmacro %}
{# Driver select for forms where a value might be preselected #} {# Driver select for forms where a value might be preselected #}
{% macro driver_select_with_preselect(driver_match, name, label, include_none, drivers=none, disabled=false, border="") %} {% macro driver_select_with_preselect(driver_match, name, label, include_none, include_inactive=false, drivers=none, disabled=false, border="") %}
<div class="form-floating"> <div class="form-floating">
<select name="{{ name }}" id="{{ name }}" class="form-select {{ border }}" aria-label="{{ name }}" {% if disabled %}disabled="disabled"{% endif %}> <select name="{{ name }}" id="{{ name }}" class="form-select {{ border }}" aria-label="{{ name }}" {% if disabled %}disabled="disabled"{% endif %}>
{# Use namespace wrapper to persist scope between loop iterations #} {# Use namespace wrapper to persist scope between loop iterations #}
{% set user_has_chosen = namespace(driverpre=false) %} {% set user_has_chosen = namespace(driverpre=false) %}
{% if drivers == none %} {% if drivers == none %}
{% set drivers = model.all_drivers(include_none=include_none, include_inactive=False) %} {% set drivers = model.all_drivers(include_none=include_none, include_inactive=include_inactive) %}
{% endif %} {% endif %}
{% for driver in drivers %} {% for driver in drivers %}

View File

@ -6,15 +6,15 @@
{% block body %} {% block body %}
<div class="card shadow-sm mb-2"> {# <div class="card shadow-sm mb-2">#}
<div class="card-header"> {# <div class="card-header">#}
Note {# Note#}
</div> {# </div>#}
{##}
<div class="card-body"> {# <div class="card-body">#}
Points only include race picks. {# Points only include race picks.#}
</div> {# </div>#}
</div> {# </div>#}
<div class="card shadow-sm mb-2"> <div class="card shadow-sm mb-2">
<div class="card-header"> <div class="card-header">
@ -29,6 +29,8 @@
<th scope="col" class="text-center" style="min-width: 50px;">Place</th> <th scope="col" class="text-center" style="min-width: 50px;">Place</th>
<th scope="col" class="text-center" style="min-width: 50px;">User</th> <th scope="col" class="text-center" style="min-width: 50px;">User</th>
<th scope="col" class="text-center" style="min-width: 100px;">Points</th> <th scope="col" class="text-center" style="min-width: 100px;">Points</th>
<th scope="col" class="text-center" style="min-width: 100px;">Points (Race)</th>
<th scope="col" class="text-center" style="min-width: 100px;">Points (Season)</th>
<th scope="col" class="text-center" style="min-width: 100px;">Total picks</th> <th scope="col" class="text-center" style="min-width: 100px;">Total picks</th>
<th scope="col" class="text-center" style="min-width: 100px;" data-bs-toggle="tooltip" <th scope="col" class="text-center" style="min-width: 100px;" data-bs-toggle="tooltip"
title="Any points count as correct">Correct picks title="Any points count as correct">Correct picks
@ -38,12 +40,14 @@
</thead> </thead>
<tbody> <tbody>
{% for user in points.users_sorted_by_points() %} {% for user in points.users_sorted_by_points(include_season=True) %}
{% set user_standing = points.user_standing()[user.name] %} {% set user_standing = points.user_standing(include_season=True)[user.name] %}
<tr class="{% if user_standing == 1 %}table-danger{% endif %}"> <tr class="{% if user_standing == 1 %}table-danger{% endif %}">
<td class="text-center text-nowrap">{{ user_standing }}</td> <td class="text-center text-nowrap">{{ user_standing }}</td>
<td class="text-center text-nowrap">{{ user.name }}</td> <td class="text-center text-nowrap">{{ user.name }}</td>
<td class="text-center text-nowrap">{{ points.total_points_by(user.name) }}</td> <td class="text-center text-nowrap">{{ points.total_points_by(user_name=user.name, include_season=True) }}</td>
<td class="text-center text-nowrap">{{ points.total_points_by(user_name=user.name, include_season=False) }}</td>
<td class="text-center text-nowrap">{{ points.season_points_by(user_name=user.name) }}</td>
<td class="text-center text-nowrap">{{ points.picks_count(user.name) }}</td> <td class="text-center text-nowrap">{{ points.picks_count(user.name) }}</td>
<td class="text-center text-nowrap">{{ points.picks_with_points_count(user.name) }}</td> <td class="text-center text-nowrap">{{ points.picks_with_points_count(user.name) }}</td>
<td class="text-center text-nowrap">{{ "%0.2f" % points.points_per_pick(user.name) }}</td> <td class="text-center text-nowrap">{{ "%0.2f" % points.points_per_pick(user.name) }}</td>
@ -57,7 +61,7 @@
<div class="card shadow-sm mb-2"> <div class="card shadow-sm mb-2">
<div class="card-header"> <div class="card-header">
History History (Race)
</div> </div>
<div class="card-body"> <div class="card-body">

View File

@ -39,13 +39,13 @@
{# Link should only be visible if all users are visible #} {# Link should only be visible if all users are visible #}
{% if model.active_user is not none %} {% if model.active_user is not none %}
<td class="text-center text-nowrap" style="min-width: 100px;">{{ model.active_user.name }} <td class="text-center text-nowrap" style="min-width: 100px;">{{ model.active_user.name }}
({{ points.total_points_by(model.active_user.name) }}) ({{ points.total_points_by(user_name=model.active_user.name, include_season=False) }})
</td> </td>
{% else %} {% else %}
{% for user in model.all_users() %} {% for user in model.all_users() %}
<td class="text-center text-nowrap" style="min-width: 100px;"> <td class="text-center text-nowrap" style="min-width: 100px;">
<a href="/race/{{ user.name_sanitized }}" class="link-dark">{{ user.name }} <a href="/race/{{ user.name_sanitized }}" class="link-dark">{{ user.name }}
({{ points.total_points_by(user.name) }})</a> ({{ points.total_points_by(user_name=user.name, include_season=False) }})</a>
</td> </td>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -10,16 +10,16 @@
{% block body %} {% block body %}
<div class="card shadow-sm mb-2"> {# <div class="card shadow-sm mb-2">#}
<div class="card-header"> {# <div class="card-header">#}
Note {# Note#}
</div> {# </div>#}
{##}
<div class="card-body"> {# <div class="card-body">#}
Picks that match the current standings are marked in green, except for the hot-take and overtake picks, as {# Picks that match the current standings are marked in green, except for the hot-take and overtake picks, as#}
those are not evaluated automatically.<br> {# those are not evaluated automatically.<br>#}
</div> {# </div>#}
</div> {# </div>#}
<div class="grid card-grid"> <div class="grid card-grid">
@ -50,7 +50,7 @@
{# Hot Take #} {# Hot Take #}
<div class="form-floating"> <div class="form-floating">
<textarea <textarea
class="form-control {% if points.hot_take_correct(user.name) %}border-success{% endif %}" class="form-control {% if points.hot_take_correct(user.name) %}border-success{% else %}border-danger{% endif %}"
id="hot-take-input-{{ user.name }}" name="hottakeselect" id="hot-take-input-{{ user.name }}" name="hottakeselect"
style="height: 150px" style="height: 150px"
{% if season_guess_open == false %}disabled="disabled"{% endif %}> {% if season_guess_open == false %}disabled="disabled"{% endif %}>
@ -64,29 +64,29 @@
<div class="mt-2"> <div class="mt-2">
{{ team_select_with_preselect(team_match=user_guess.p2_wcc, name="p2select", label="P2 in WCC:", {{ team_select_with_preselect(team_match=user_guess.p2_wcc, name="p2select", label="P2 in WCC:",
include_none=false, disabled=not season_guess_open, include_none=false, disabled=not season_guess_open,
border=("border-success" if points.p2_constructor_correct(user.name) else "")) }} border=("border-success" if points.p2_constructor_correct(user.name) else "border-danger")) }}
</div> </div>
{# Most Overtakes + DNFs #} {# Most Overtakes + DNFs #}
<div class="input-group mt-2"> <div class="input-group mt-2">
{{ driver_select_with_preselect(driver_match=user_guess.most_overtakes, name="overtakeselect", {{ driver_select_with_preselect(driver_match=user_guess.most_overtakes, name="overtakeselect",
label="Most overtakes:", include_none=false, disabled=not season_guess_open, label="Most overtakes:", include_none=false, include_inactive=true, disabled=not season_guess_open,
border=("border-success" if points.overtakes_correct(user.name) else "")) }} border=("border-success" if points.overtakes_correct(user.name) else "border-danger")) }}
{{ driver_select_with_preselect(driver_match=user_guess.most_dnfs, name="dnfselect", label="Most DNFs:", {{ driver_select_with_preselect(driver_match=user_guess.most_dnfs, name="dnfselect", label="Most DNFs:",
include_none=false, disabled=not season_guess_open, include_none=false, include_inactive=true, disabled=not season_guess_open,
border=("border-success" if points.dnfs_correct(user.name) else "")) }} border=("border-success" if points.dnfs_correct(user.name) else "border-danger")) }}
</div> </div>
{# Most Gained + Lost #} {# Most Gained + Lost #}
<div class="input-group mt-2" data-bs-toggle="tooltip" <div class="input-group mt-2" data-bs-toggle="tooltip"
title="Which driver will gain/lose the most places in comparison to last season's results?"> title="Which driver will gain/lose the most places in comparison to last season's results?">
{{ driver_select_with_preselect(driver_match=user_guess.most_wdc_gained, name="gainedselect", {{ driver_select_with_preselect(driver_match=user_guess.most_wdc_gained, name="gainedselect",
label="Most WDC pl. gained:", include_none=false, drivers=model.active_drivers_for_wdc_gained(), label="Most WDC pl. gained:", include_none=false, include_inactive=true,
disabled=not season_guess_open, drivers=model.active_drivers_for_wdc_gained(), disabled=not season_guess_open,
border=("border-success" if points.most_gained_correct(user.name) else "")) }} border=("border-success" if points.most_gained_correct(user.name) else "border-danger")) }}
{{ driver_select_with_preselect(driver_match=user_guess.most_wdc_lost, name="lostselect", {{ driver_select_with_preselect(driver_match=user_guess.most_wdc_lost, name="lostselect",
label="Most WDC pl. lost:", include_none=false, disabled=not season_guess_open, label="Most WDC pl. lost:", include_none=false, include_inactive=true, disabled=not season_guess_open,
border=("border-success" if points.most_lost_correct(user.name) else "")) }} border=("border-success" if points.most_lost_correct(user.name) else "border-danger")) }}
</div> </div>
{# Team-internal Winners #} {# Team-internal Winners #}
@ -95,8 +95,10 @@
winners:</h6> winners:</h6>
<div class="grid mt-2 container" style="row-gap: 0;"> <div class="grid mt-2 container" style="row-gap: 0;">
{% for team in model.all_teams(include_none=false) %} {% for team in model.all_teams(include_none=false) %}
{% set driver_a = model.drivers_by(team_name=team.name, include_inactive=False)[0] %} {# HACK: Choosing 0 and 1 will chose the drivers with the lowest IDs (although there could be others). #}
{% set driver_b = model.drivers_by(team_name=team.name, include_inactive=False)[1] %} {# This means the drivers chosen from at the start of the season will be visible. #}
{% set driver_a = model.drivers_by(team_name=team.name, include_inactive=True)[0] %}
{% set driver_b = model.drivers_by(team_name=team.name, include_inactive=True)[1] %}
<div class="g-col-6"> <div class="g-col-6">
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
@ -106,7 +108,8 @@
value="{{ driver_a.id }}" value="{{ driver_a.id }}"
{% if (user_guess is not none) and (driver_a in user_guess.team_winners) %}checked="checked"{% endif %} {% if (user_guess is not none) and (driver_a in user_guess.team_winners) %}checked="checked"{% endif %}
{% if season_guess_open == false %}disabled="disabled"{% endif %}> {% if season_guess_open == false %}disabled="disabled"{% endif %}>
<label class="form-check-label {% if (user_guess is not none) and (driver_a in user_guess.team_winners) and points.is_team_winner(driver_a) %}text-success{% endif %}" <label class="form-check-label {% if (user_guess is not none) and (driver_a in user_guess.team_winners) and points.is_team_winner(driver_a) %}text-success
{% elif (user_guess is not none) and (driver_a in user_guess.team_winners) and (not points.is_team_winner(driver_a)) %}text-danger{% endif %}"
for="teamwinner-{{ team.id }}-1-{{ user.id }}">{{ driver_a.name }}</label> for="teamwinner-{{ team.id }}-1-{{ user.id }}">{{ driver_a.name }}</label>
</div> </div>
</div> </div>
@ -119,7 +122,8 @@
value="{{ driver_b.id }}" value="{{ driver_b.id }}"
{% if (user_guess is not none) and (driver_b in user_guess.team_winners) %}checked="checked"{% endif %} {% if (user_guess is not none) and (driver_b in user_guess.team_winners) %}checked="checked"{% endif %}
{% if season_guess_open == false %}disabled="disabled"{% endif %}> {% if season_guess_open == false %}disabled="disabled"{% endif %}>
<label class="form-check-label {% if (user_guess is not none) and (driver_b in user_guess.team_winners) and points.is_team_winner(driver_b) %}text-success{% endif %}" <label class="form-check-label {% if (user_guess is not none) and (driver_b in user_guess.team_winners) and points.is_team_winner(driver_b) %}text-success
{% elif (user_guess is not none) and (driver_b in user_guess.team_winners) and (not points.is_team_winner(driver_b)) %}text-danger{% endif %}"
for="teamwinner-{{ team.id }}-2-{{ user.id }}">{{ driver_b.name }}</label> for="teamwinner-{{ team.id }}-2-{{ user.id }}">{{ driver_b.name }}</label>
</div> </div>
</div> </div>
@ -131,8 +135,10 @@
title="Which driver will reach at least a single podium?">Drivers with podium(s):</h6> title="Which driver will reach at least a single podium?">Drivers with podium(s):</h6>
<div class="grid mt-2 container" style="row-gap: 0;"> <div class="grid mt-2 container" style="row-gap: 0;">
{% for team in model.all_teams(include_none=false) %} {% for team in model.all_teams(include_none=false) %}
{% set driver_a = model.drivers_by(team_name=team.name, include_inactive=False)[0] %} {# HACK: Choosing 0 and 1 will chose the drivers with the lowest IDs (although there could be others). #}
{% set driver_b = model.drivers_by(team_name=team.name, include_inactive=False)[1] %} {# This means the drivers chosen from at the start of the season will be visible. #}
{% set driver_a = model.drivers_by(team_name=team.name, include_inactive=True)[0] %}
{% set driver_b = model.drivers_by(team_name=team.name, include_inactive=True)[1] %}
<div class="g-col-6"> <div class="g-col-6">
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
@ -142,7 +148,9 @@
value="{{ driver_a.id }}" value="{{ driver_a.id }}"
{% if (user_guess is not none) and (driver_a in user_guess.podiums) %}checked="checked"{% endif %} {% if (user_guess is not none) and (driver_a in user_guess.podiums) %}checked="checked"{% endif %}
{% if season_guess_open == false %}disabled="disabled"{% endif %}> {% if season_guess_open == false %}disabled="disabled"{% endif %}>
<label class="form-check-label {% if (user_guess is not none) and (driver_a in user_guess.podiums) and points.has_podium(driver_a) %}text-success{% endif %}" <label class="form-check-label {% if (user_guess is not none) and (driver_a in user_guess.podiums) and points.has_podium(driver_a) %}text-success
{% elif (user_guess is not none) and (driver_a in user_guess.podiums) and (not points.has_podium(driver_a)) %}text-danger
{% elif (user_guess is not none) and (driver_a not in user_guess.podiums) and (points.has_podium(driver_a)) %}text-danger{% endif %}"
for="podium-{{ driver_a.id }}-{{ user.id }}">{{ driver_a.name }}</label> for="podium-{{ driver_a.id }}-{{ user.id }}">{{ driver_a.name }}</label>
</div> </div>
</div> </div>
@ -155,7 +163,9 @@
value="{{ driver_b.id }}" value="{{ driver_b.id }}"
{% if (user_guess is not none) and (driver_b in user_guess.podiums) %}checked="checked"{% endif %} {% if (user_guess is not none) and (driver_b in user_guess.podiums) %}checked="checked"{% endif %}
{% if season_guess_open == false %}disabled="disabled"{% endif %}> {% if season_guess_open == false %}disabled="disabled"{% endif %}>
<label class="form-check-label {% if (user_guess is not none) and (driver_b in user_guess.podiums) and points.has_podium(driver_b) %}text-success{% endif %}" <label class="form-check-label {% if (user_guess is not none) and (driver_b in user_guess.podiums) and points.has_podium(driver_b) %}text-success
{% elif (user_guess is not none) and (driver_b in user_guess.podiums) and (not points.has_podium(driver_b)) %}text-danger
{% elif (user_guess is not none) and (driver_b not in user_guess.podiums) and (points.has_podium(driver_b)) %}text-danger{% endif %}"
for="podium-{{ driver_b.id }}-{{ user.id }}">{{ driver_b.name }}</label> for="podium-{{ driver_b.id }}-{{ user.id }}">{{ driver_b.name }}</label>
</div> </div>
</div> </div>