Compare commits

..

3 Commits

Author SHA1 Message Date
4b3f95764c Disable stuff no longer required
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-09 20:30:35 +01:00
4fc13471f5 Enable docker workflow on all branches
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 15s
2024-03-09 20:05:34 +01:00
8905673d6d Add requests dependency 2024-03-09 20:04:57 +01:00
33 changed files with 249 additions and 741 deletions

View File

@ -2,7 +2,7 @@ name: Build Formula10 Docker Image
on: on:
push: push:
branches: [main] # branches: [main]
paths: paths:
- ".gitea/workflows/**" - ".gitea/workflows/**"
- "Dockerfile" - "Dockerfile"

View File

@ -42,26 +42,11 @@
# Use $1 for positional args # Use $1 for positional args
commands = [ commands = [
{ # {
name = "vscode"; # name = "";
help = "Launch VSCode"; # help = "";
command = "code . &>/dev/null &"; # command = "";
} # }
{
name = "pycharm";
help = "Launch PyCharm Professional";
command = "pycharm-professional . &>/dev/null &";
}
{
name = "db";
help = "Launch SQLiteBrowser";
command = "sqlitebrowser ./instance/formula10.db &>/dev/null &";
}
{
name = "api";
help = "Launch Hoppscotch in Google Chrome";
command = "google-chrome-stable https://hoppscotch.io &>/dev/null &";
}
]; ];
}; };
}); });

View File

@ -9,12 +9,12 @@ if not ENABLE_TIMING:
print("- Disabled timing constraints") print("- Disabled timing constraints")
app: Flask = Flask(__name__) app: Flask = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///formula10.db" app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///formula10.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Session cookie is used to propagate message to error page # Session cookie is used to propagate message to error page
app.config["SESSION_TYPE"] = "memcached" app.config['SESSION_TYPE'] = 'memcached'
app.config["SECRET_KEY"] = "ich stinke nach maggi" app.config['SECRET_KEY'] = 'ich stinke nach maggi'
app.url_map.strict_slashes = False app.url_map.strict_slashes = False
@ -29,24 +29,3 @@ import formula10.controller.statistics_controller
import formula10.controller.rules_controller import formula10.controller.rules_controller
import formula10.controller.admin_controller import formula10.controller.admin_controller
import formula10.controller.error_controller import formula10.controller.error_controller
# TODO
# Large DB Update
# - Don't use names for frontend post requests, use IDs
# - For season guess calc there is missing: Fastest laps + sprint points + sprint DNFs (in race result)
# - Mask to allow changing usernames (easy if name is not used as ID)
# - Maybe even masks for races + drivers + teams?
# - DB fields for links to F1 site - NO: just hardcode them in with a dictionary
# Leaderboards/Points
# - Auto calculate season points (display season points in table + season guess card title?)
# Optimizations
# - Optimize PointsModel + TemplateModel. In case something is calculated often, cache it.
# - NEVER do manual DB queries, except in the DomainModel!
# General
# - Adapt diagram colors to team colors
# - Add links to the official F1 stats page (for quali/result), probably best to store entire link in DB (because they are not entirely regular)?
# - Unit testing (as much as possible, but especially points calculation)

View File

@ -1,61 +1,36 @@
from typing import List from flask import render_template, request
from urllib.parse import unquote
from flask import redirect, render_template, request
from werkzeug import Response from werkzeug import Response
from formula10.controller.error_controller import error_redirect from formula10.database.update_queries import update_user
from formula10.database.update_queries import update_race_result, update_user
from formula10.domain.domain_model import Model
from formula10.domain.template_model import TemplateModel from formula10.domain.template_model import TemplateModel
from formula10 import app from formula10 import app
from formula10.openf1.model.api_session import ApiSession
from formula10.openf1.openf1_definitions import OPENF1_SESSION_NAME_RACE
from formula10.openf1.openf1_fetcher import fetch_openf1_driver, fetch_openf1_position, fetch_openf1_session
@app.route("/result") # @app.route("/result")
def result_root() -> Response: # def result_root() -> Response:
return redirect("/result/Current") # return redirect("/result/Current")
@app.route("/result/<race_name>") # @app.route("/result/<race_name>")
def result_active_race(race_name: str) -> str: # def result_active_race(race_name: str) -> str:
race_name = unquote(race_name) # race_name = unquote(race_name)
model = TemplateModel(active_user_name=None, # model = TemplateModel(active_user_name=None,
active_result_race_name=race_name) # active_result_race_name=race_name)
return render_template("result.jinja", model=model) # return render_template("result.jinja", model=model)
@app.route("/result-enter/<race_name>", methods=["POST"]) # @app.route("/result-enter/<race_name>", methods=["POST"])
def result_enter_post(race_name: str) -> Response: # def result_enter_post(race_name: str) -> Response:
race_name = unquote(race_name) # race_name = unquote(race_name)
pxxs: List[str] = request.form.getlist("pxx-drivers") # pxxs: List[str] = request.form.getlist("pxx-drivers")
first_dnfs: List[str] = request.form.getlist("first-dnf-drivers") # first_dnfs: List[str] = request.form.getlist("first-dnf-drivers")
dnfs: List[str] = request.form.getlist("dnf-drivers") # dnfs: List[str] = request.form.getlist("dnf-drivers")
excluded: List[str] = request.form.getlist("excluded-drivers") # excluded: List[str] = request.form.getlist("excluded-drivers")
# Extra stats for points calculation # # @todo Ugly
fastest_lap: str | None = request.form.get("fastest-lap") # race_id: int = Model().race_by(race_name=race_name).id
sprint_pxxs: List[str] = request.form.getlist("sprint-pxx-drivers") # return update_race_result(race_id, pxxs, first_dnfs, dnfs, excluded)
sprint_dnf_drivers: List[str] = request.form.getlist("sprint-dnf-drivers")
if fastest_lap is None:
return error_redirect("Data was not saved, because fastest lap was not set.")
race_id: int = Model().race_by(race_name=race_name).id
return update_race_result(race_id, pxxs, first_dnfs, dnfs, excluded, int(fastest_lap), sprint_pxxs, sprint_dnf_drivers)
@app.route("/result-fetch/<race_name>", methods=["POST"])
def result_fetch_post(race_name: str) -> Response:
session: ApiSession = fetch_openf1_session(OPENF1_SESSION_NAME_RACE, "KSA")
fetch_openf1_driver(session.session_key, "VER")
fetch_openf1_position(session.session_key, 1)
# @todo Fetch stuff and build the race_result using update_race_result(...)
return redirect("/result")
@app.route("/user") @app.route("/user")

View File

@ -1,4 +1,4 @@
from sqlalchemy import Integer, String, ForeignKey, Boolean from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy.orm import mapped_column, Mapped, relationship from sqlalchemy.orm import mapped_column, Mapped, relationship
from formula10.database.model.db_team import DbTeam from formula10.database.model.db_team import DbTeam
@ -20,7 +20,6 @@ class DbDriver(db.Model):
abbr: Mapped[str] = mapped_column(String(4), nullable=False, unique=True) abbr: Mapped[str] = mapped_column(String(4), nullable=False, unique=True)
team_id: Mapped[str] = mapped_column(ForeignKey("team.id"), nullable=False) team_id: Mapped[str] = mapped_column(ForeignKey("team.id"), nullable=False)
country_code: Mapped[str] = mapped_column(String(2), nullable=False) # alpha-2 code country_code: Mapped[str] = mapped_column(String(2), nullable=False) # alpha-2 code
active: Mapped[bool] = mapped_column(Boolean, nullable=False)
# Relationships # Relationships
team: Mapped[DbTeam] = relationship("DbTeam", foreign_keys=[team_id]) team: Mapped[DbTeam] = relationship("DbTeam", foreign_keys=[team_id])

View File

@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from sqlalchemy import Boolean, DateTime, Integer, String from sqlalchemy import DateTime, Integer, String
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from formula10 import db from formula10 import db
@ -20,5 +20,3 @@ class DbRace(db.Model):
number: Mapped[int] = mapped_column(Integer, nullable=False, unique=True) number: Mapped[int] = mapped_column(Integer, nullable=False, unique=True)
date: Mapped[datetime] = mapped_column(DateTime, nullable=False, unique=True) date: Mapped[datetime] = mapped_column(DateTime, nullable=False, unique=True)
pxx: Mapped[int] = mapped_column(Integer, nullable=False) # This is the place to guess pxx: Mapped[int] = mapped_column(Integer, nullable=False) # This is the place to guess
quali_date: Mapped[datetime] = mapped_column(DateTime, nullable=False, unique=True)
has_sprint: Mapped[bool] = mapped_column(Boolean, nullable=False)

View File

@ -1,17 +1,14 @@
import json import json
from typing import Dict, List, cast from typing import List, cast
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.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 import DbRace
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_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, race_has_started from formula10.database.validation import any_is_none, race_has_started
from formula10 import ENABLE_TIMING, db from formula10 import ENABLE_TIMING, db
@ -115,82 +112,77 @@ def update_season_guess(user_id: int, guesses: List[str | None], team_winner_gue
return redirect(f"/season/Everyone") return redirect(f"/season/Everyone")
def find_or_create_race_result(race_id: int) -> DbRaceResult: # def find_or_create_race_result(race_id: int) -> DbRaceResult:
# There can be a single RaceResult at most, since race_name is the primary key # # There can be a single RaceResult at most, since race_name is the primary key
race_result: DbRaceResult | None = db.session.query(DbRaceResult).filter_by(race_id=race_id).first() # race_result: DbRaceResult | None = db.session.query(DbRaceResult).filter_by(race_id=race_id).first()
if race_result is not None: # if race_result is not None:
return race_result # return race_result
race_result = DbRaceResult(race_id=race_id) # race_result = DbRaceResult(race_id=race_id)
race_result.pxx_driver_ids_json = json.dumps(["9999"]) # race_result.pxx_driver_ids_json = json.dumps(["9999"])
race_result.first_dnf_driver_ids_json = json.dumps(["9999"]) # race_result.first_dnf_driver_ids_json = json.dumps(["9999"])
race_result.dnf_driver_ids_json = json.dumps(["9999"]) # race_result.dnf_driver_ids_json = json.dumps(["9999"])
race_result.excluded_driver_ids_json = json.dumps(["9999"]) # race_result.excluded_driver_ids_json = json.dumps(["9999"])
race_result.fastest_lap_id = 9999 # race_result.fastest_lap_id = 9999
race_result.sprint_dnf_driver_ids_json = json.dumps([]) # race_result.sprint_dnf_driver_ids_json = json.dumps(["9999"])
race_result.sprint_points_json = json.dumps({}) # race_result.sprint_points_json = json.dumps({"9999": "9999"})
db.session.add(race_result) # db.session.add(race_result)
db.session.commit() # db.session.commit()
# Double check if database insertion worked and obtain any values set by the database # # Double check if database insertion worked and obtain any values set by the database
race_result = db.session.query(DbRaceResult).filter_by(race_id=race_id).first() # race_result = db.session.query(DbRaceResult).filter_by(race_id=race_id).first()
if race_result is None: # if race_result is None:
raise Exception("Failed adding RaceResult to the database") # raise Exception("Failed adding RaceResult to the database")
return race_result # return race_result
def update_race_result(race_id: int, pxx_driver_ids_list: List[str], first_dnf_driver_ids_list: List[str], dnf_driver_ids_list: List[str], excluded_driver_ids_list: List[str], # def update_race_result(race_id: int, pxx_driver_ids_list: List[str], first_dnf_driver_ids_list: List[str], dnf_driver_ids_list: List[str], excluded_driver_ids_list: List[str]) -> Response:
fastest_lap_driver_id: int, sprint_pxx_driver_ids_list: List[str], sprint_dnf_driver_ids_list: List[str]) -> Response: # if ENABLE_TIMING and not race_has_started(race_id=race_id):
if ENABLE_TIMING and not race_has_started(race_id=race_id): # return error_redirect("No race result can be entered, as the race has not begun!")
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_ids: Dict[str, str] = { # pxx_driver_names: Dict[str, str] = {
str(position + 1): driver_id for position, driver_id in enumerate(pxx_driver_ids_list) # str(position + 1): driver_id for position, driver_id in enumerate(pxx_driver_ids_list)
} # }
# Not counted drivers have to be at the end # # Not counted drivers have to be at the end
excluded_driver_ids: Dict[str, str] = { # excluded_driver_names: Dict[str, str] = {
str(position + 1): driver_id for position, driver_id in enumerate(pxx_driver_ids_list) # str(position + 1): driver_id for position, driver_id in enumerate(pxx_driver_ids_list)
if driver_id in excluded_driver_ids_list # if driver_id in excluded_driver_ids_list
} # }
if len(excluded_driver_ids) > 0 and (not "20" in excluded_driver_ids or not positions_are_contiguous(list(excluded_driver_ids.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 error_redirect("Race result was not saved, as excluded drivers must be contiguous and at the end of the field!") # 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_id in first_dnf_driver_ids_list: # for driver_id in first_dnf_driver_ids_list:
if driver_id not in dnf_driver_ids_list: # if driver_id not in dnf_driver_ids_list:
dnf_driver_ids_list.append(driver_id) # dnf_driver_ids_list.append(driver_id)
# There can't be dnfs but no initial dnfs # # There can't be dnfs but no initial dnfs
if len(dnf_driver_ids_list) > 0 and len(first_dnf_driver_ids_list) == 0: # if len(dnf_driver_ids_list) > 0 and len(first_dnf_driver_ids_list) == 0:
return error_redirect("Race result was not saved, as there cannot be DNFs without (an) initial DNF(s)!") # 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_id) # race_result: DbRaceResult = find_or_create_race_result(race_id)
race_result.pxx_driver_ids_json = json.dumps(pxx_driver_ids) # race_result.pxx_driver_ids_json = json.dumps(pxx_driver_names)
race_result.first_dnf_driver_ids_json = json.dumps(first_dnf_driver_ids_list) # race_result.first_dnf_driver_ids_json = json.dumps(first_dnf_driver_ids_list)
race_result.dnf_driver_ids_json = json.dumps(dnf_driver_ids_list) # race_result.dnf_driver_ids_json = json.dumps(dnf_driver_ids_list)
race_result.excluded_driver_ids_json = json.dumps(excluded_driver_ids_list) # race_result.excluded_driver_ids_json = json.dumps(excluded_driver_ids_list)
# Extra stats for points calculation # # @todo Dummy values
sprint_pxx_driver_ids: Dict[str, str] = { # race_result.fastest_lap_id = NONE_DRIVER.id
str(position + 1): driver_id for position, driver_id in enumerate(sprint_pxx_driver_ids_list) # race_result.sprint_dnf_driver_ids_json = json.dumps([NONE_DRIVER.id])
} # race_result.sprint_points_json = json.dumps({NONE_DRIVER.id: 0})
race_result.fastest_lap_id = fastest_lap_driver_id # db.session.commit()
race_result.sprint_dnf_driver_ids_json = json.dumps(sprint_dnf_driver_ids_list)
race_result.sprint_points_json = json.dumps(sprint_pxx_driver_ids)
db.session.commit() # race: DbRace | None = db.session.query(DbRace).filter_by(id=race_id).first()
# if race is None:
# raise Exception(f"Could not find DbRace with id {race_id}")
race: DbRace | None = db.session.query(DbRace).filter_by(id=race_id).first() # return redirect(f"/result/{quote(race.name)}")
if race is None:
raise Exception(f"Could not find DbRace with id {race_id}")
return redirect(f"/result/{quote(race.name)}")
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:

View File

@ -76,19 +76,6 @@ def find_multiple_strict(predicate: Callable[[_T], bool], iterable: Iterable[_T]
return filtered return filtered
def find_atleast_strict(predicate: Callable[[_T], bool], iterable: Iterable[_T], count: int = 0) -> List[_T]:
"""
Finds at least <count> elements in a sequence matching a predicate.
Throws exception if fewer elements were found than specified.
"""
filtered = list(filter(predicate, iterable))
if len(filtered) < count:
raise Exception(f"find_atleast found {len(filtered)} matching elements but expected at least {count}")
return filtered
def find_single_strict(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T: def find_single_strict(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T:
""" """
Find a single element in a sequence matching a predicate. Find a single element in a sequence matching a predicate.

View File

@ -9,7 +9,7 @@ from formula10.database.model.db_season_guess import DbSeasonGuess
from formula10.database.model.db_season_guess_result import DbSeasonGuessResult from formula10.database.model.db_season_guess_result import DbSeasonGuessResult
from formula10.database.model.db_team import DbTeam from formula10.database.model.db_team import DbTeam
from formula10.database.model.db_user import DbUser from formula10.database.model.db_user import DbUser
from formula10.database.validation import find_multiple_strict, find_single_or_none_strict, find_single_strict, find_atleast_strict from formula10.database.validation import find_multiple_strict, find_single_or_none_strict, find_single_strict
from formula10.domain.model.driver import NONE_DRIVER, Driver from formula10.domain.model.driver import NONE_DRIVER, Driver
from formula10.domain.model.race import Race from formula10.domain.model.race import Race
from formula10.domain.model.race_guess import RaceGuess from formula10.domain.model.race_guess import RaceGuess
@ -21,7 +21,7 @@ from formula10.domain.model.user import User
from formula10 import db from formula10 import db
class Model: class Model():
_all_users: List[User] | None = None _all_users: List[User] | None = None
_all_race_results: List[RaceResult] | None = None _all_race_results: List[RaceResult] | None = None
_all_race_guesses: List[RaceGuess] | None = None _all_race_guesses: List[RaceGuess] | None = None
@ -29,7 +29,6 @@ class Model:
_all_season_guess_results: List[SeasonGuessResult] | None = None _all_season_guess_results: List[SeasonGuessResult] | None = None
_all_races: List[Race] | None = None _all_races: List[Race] | None = None
_all_drivers: List[Driver] | None = None _all_drivers: List[Driver] | None = None
_all_active_drivers: List[Driver] | None = None
_all_teams: List[Team] | None = None _all_teams: List[Team] | None = None
def all_users(self) -> List[User]: def all_users(self) -> List[User]:
@ -101,34 +100,21 @@ class Model:
return self._all_races return self._all_races
def all_drivers(self, *, include_none: bool, include_inactive: bool) -> List[Driver]: def all_drivers(self, *, include_none: bool) -> List[Driver]:
""" """
Returns a list of all active drivers. Returns a list of all drivers.
""" """
if include_inactive: if self._all_drivers is None:
if self._all_drivers is None: self._all_drivers = [
self._all_drivers = [ Driver.from_db_driver(db_driver)
Driver.from_db_driver(db_driver) for db_driver in db.session.query(DbDriver).all()
for db_driver in db.session.query(DbDriver).all() ]
]
if include_none: if include_none:
return self._all_drivers return self._all_drivers
else:
predicate: Callable[[Driver], bool] = lambda driver: driver != NONE_DRIVER
return find_multiple_strict(predicate, self._all_drivers)
else: else:
if self._all_active_drivers is None: predicate: Callable[[Driver], bool] = lambda driver: driver != NONE_DRIVER
self._all_active_drivers = [ return find_multiple_strict(predicate, self._all_drivers)
Driver.from_db_driver(db_driver)
for db_driver in db.session.query(DbDriver).filter_by(active=True).all()
]
if include_none:
return self._all_active_drivers
else:
predicate: Callable[[Driver], bool] = lambda driver: driver != NONE_DRIVER
return find_multiple_strict(predicate, self._all_active_drivers)
def all_teams(self, *, include_none: bool) -> List[Team]: def all_teams(self, *, include_none: bool) -> List[Team]:
""" """
@ -294,40 +280,34 @@ class Model:
# Team queries # Team queries
# #
@staticmethod def none_team(self) -> Team:
def none_team() -> Team:
return NONE_TEAM return NONE_TEAM
# #
# Driver queries # Driver queries
# #
@staticmethod def none_driver(self) -> Driver:
def none_driver() -> Driver:
return NONE_DRIVER return NONE_DRIVER
@overload @overload
def drivers_by(self, *, team_name: str, include_inactive: bool) -> List[Driver]: def drivers_by(self, *, team_name: str) -> List[Driver]:
""" """
Returns a list of all drivers driving for a certain team. Returns a list of all drivers driving for a certain team.
""" """
return self.drivers_by(team_name=team_name, include_inactive=include_inactive) return self.drivers_by(team_name=team_name)
@overload @overload
def drivers_by(self, *, include_inactive: bool) -> Dict[str, List[Driver]]: def drivers_by(self) -> Dict[str, List[Driver]]:
""" """
Returns a dictionary of drivers mapped to team names. Returns a dictionary of drivers mapped to team names.
""" """
return self.drivers_by(include_inactive=include_inactive) return self.drivers_by()
def drivers_by(self, *, team_name: str | None = None, include_inactive: bool) -> List[Driver] | Dict[str, List[Driver]]: def drivers_by(self, *, team_name: str | None = None) -> List[Driver] | Dict[str, List[Driver]]:
if team_name is not None: if team_name is not None:
predicate: Callable[[Driver], bool] = lambda driver: driver.team.name == team_name predicate: Callable[[Driver], bool] = lambda driver: driver.team.name == team_name
return find_multiple_strict(predicate, self.all_drivers(include_none=False), 2)
if include_inactive:
return find_atleast_strict(predicate, self.all_drivers(include_none=False, include_inactive=True), 2)
else:
return find_multiple_strict(predicate, self.all_drivers(include_none=False, include_inactive=False), 2)
if team_name is None: if team_name is None:
drivers_by: Dict[str, List[Driver]] = dict() drivers_by: Dict[str, List[Driver]] = dict()
@ -336,7 +316,7 @@ class Model:
for team in self.all_teams(include_none=False): for team in self.all_teams(include_none=False):
drivers_by[team.name] = [] drivers_by[team.name] = []
for driver in self.all_drivers(include_none=False, include_inactive=include_inactive): for driver in self.all_drivers(include_none=False):
drivers_by[driver.team.name] += [driver] drivers_by[driver.team.name] += [driver]
return drivers_by return drivers_by

View File

@ -4,7 +4,7 @@ from formula10.database.model.db_driver import DbDriver
from formula10.domain.model.team import NONE_TEAM, Team from formula10.domain.model.team import NONE_TEAM, Team
class Driver: class Driver():
@classmethod @classmethod
def from_db_driver(cls, db_driver: DbDriver): def from_db_driver(cls, db_driver: DbDriver):
driver: Driver = cls() driver: Driver = cls()
@ -13,7 +13,6 @@ class Driver:
driver.abbr = db_driver.abbr driver.abbr = db_driver.abbr
driver.country = db_driver.country_code driver.country = db_driver.country_code
driver.team = Team.from_db_team(db_driver.team) driver.team = Team.from_db_team(db_driver.team)
driver.active = db_driver.active
return driver return driver
def to_db_driver(self) -> DbDriver: def to_db_driver(self) -> DbDriver:
@ -22,7 +21,6 @@ class Driver:
db_driver.abbr = self.abbr db_driver.abbr = self.abbr
db_driver.country_code = self.country db_driver.country_code = self.country
db_driver.team_id = self.team.name db_driver.team_id = self.team.name
db_driver.active = self.active
return db_driver return db_driver
def __eq__(self, __value: object) -> bool: def __eq__(self, __value: object) -> bool:
@ -39,7 +37,6 @@ class Driver:
abbr: str abbr: str
country: str country: str
team: Team team: Team
active: bool
@property @property
def name_sanitized(self) -> str: def name_sanitized(self) -> str:
@ -52,4 +49,3 @@ NONE_DRIVER.name = "None"
NONE_DRIVER.abbr = "None" NONE_DRIVER.abbr = "None"
NONE_DRIVER.country = "NO" NONE_DRIVER.country = "NO"
NONE_DRIVER.team = NONE_TEAM NONE_DRIVER.team = NONE_TEAM
NONE_DRIVER.active = False

View File

@ -4,7 +4,7 @@ from urllib.parse import quote
from formula10.database.model.db_race import DbRace from formula10.database.model.db_race import DbRace
class Race: class Race():
@classmethod @classmethod
def from_db_race(cls, db_race: DbRace): def from_db_race(cls, db_race: DbRace):
race: Race = cls() race: Race = cls()
@ -13,8 +13,6 @@ class Race:
race.number = db_race.number race.number = db_race.number
race.date = db_race.date race.date = db_race.date
race.place_to_guess = db_race.pxx race.place_to_guess = db_race.pxx
race.quali_date = db_race.quali_date
race.has_sprint = db_race.has_sprint
return race return race
def to_db_race(self) -> DbRace: def to_db_race(self) -> DbRace:
@ -23,8 +21,6 @@ class Race:
db_race.number = self.number db_race.number = self.number
db_race.date = self.date db_race.date = self.date
db_race.pxx = self.place_to_guess db_race.pxx = self.place_to_guess
db_race.quali_date = self.date
db_race.has_sprint = self.has_sprint
return db_race return db_race
def __eq__(self, __value: object) -> bool: def __eq__(self, __value: object) -> bool:
@ -41,8 +37,6 @@ class Race:
number: int number: int
date: datetime date: datetime
place_to_guess: int place_to_guess: int
quali_date: datetime
has_sprint: bool
@property @property
def name_sanitized(self) -> str: def name_sanitized(self) -> str:

View File

@ -4,7 +4,7 @@ from formula10.domain.model.race import Race
from formula10.domain.model.user import User from formula10.domain.model.user import User
class RaceGuess: class RaceGuess():
@classmethod @classmethod
def from_db_race_guess(cls, db_race_guess: DbRaceGuess): def from_db_race_guess(cls, db_race_guess: DbRaceGuess):
race_guess: RaceGuess = cls() race_guess: RaceGuess = cls()

View File

@ -154,8 +154,6 @@ class RaceResult:
return points_strings[position_offset] return points_strings[position_offset]
else: else:
return "0 Points" return "0 Points"
elif driver == _driver and driver in self.standing_exclusions:
return "0 Points"
raise Exception(f"Could not get points string for driver {driver.name}") raise Exception(f"Could not get points string for driver {driver.name}")
@ -176,11 +174,6 @@ class RaceResult:
self.standing[str(position)] for position in range(1, 21) self.standing[str(position)] for position in range(1, 21)
] ]
def ordered_sprint_standing_list(self) -> List[Driver]:
return [
self.sprint_standing[str(position)] for position in range(1, 21)
]
def initial_dnf_string(self) -> str: def initial_dnf_string(self) -> str:
if len(self.initial_dnf) == 0: if len(self.initial_dnf) == 0:
return NONE_DRIVER.name return NONE_DRIVER.name

View File

@ -7,7 +7,7 @@ from formula10.domain.model.team import Team
from formula10.domain.model.user import User from formula10.domain.model.user import User
class SeasonGuess: class SeasonGuess():
@classmethod @classmethod
def from_db_season_guess(cls, db_season_guess: DbSeasonGuess): def from_db_season_guess(cls, db_season_guess: DbSeasonGuess):
season_guess: SeasonGuess = cls() season_guess: SeasonGuess = cls()

View File

@ -2,7 +2,7 @@ from formula10.database.model.db_season_guess_result import DbSeasonGuessResult
from formula10.domain.model.user import User from formula10.domain.model.user import User
class SeasonGuessResult: class SeasonGuessResult():
@classmethod @classmethod
def from_db_season_guess_result(cls, db_season_guess_result: DbSeasonGuessResult): def from_db_season_guess_result(cls, db_season_guess_result: DbSeasonGuessResult):
season_guess_result: SeasonGuessResult = cls() season_guess_result: SeasonGuessResult = cls()

View File

@ -3,7 +3,7 @@ from urllib.parse import quote
from formula10.database.model.db_team import DbTeam from formula10.database.model.db_team import DbTeam
class Team: class Team():
@classmethod @classmethod
def from_db_team(cls, db_team: DbTeam): def from_db_team(cls, db_team: DbTeam):
team: Team = cls() team: Team = cls()

View File

@ -3,7 +3,7 @@ from urllib.parse import quote
from formula10.database.model.db_user import DbUser from formula10.database.model.db_user import DbUser
class User: class User():
@classmethod @classmethod
def from_db_user(cls, db_user: DbUser): def from_db_user(cls, db_user: DbUser):
user: User = cls() user: User = cls()

View File

@ -11,8 +11,6 @@ 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
# Guess points
RACE_GUESS_OFFSET_POINTS: Dict[int, int] = { RACE_GUESS_OFFSET_POINTS: Dict[int, int] = {
3: 1, 3: 1,
2: 3, 2: 3,
@ -20,6 +18,7 @@ RACE_GUESS_OFFSET_POINTS: Dict[int, int] = {
0: 10 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
SEASON_GUESS_OVERTAKES_POINTS: int = 10 SEASON_GUESS_OVERTAKES_POINTS: int = 10
@ -31,8 +30,6 @@ SEASON_GUESS_TEAMWINNER_FALSE_POINTS: int = -3
SEASON_GUESS_PODIUMS_CORRECT_POINTS: int = 3 SEASON_GUESS_PODIUMS_CORRECT_POINTS: int = 3
SEASON_GUESS_PODIUMS_FALSE_POINTS: int = -2 SEASON_GUESS_PODIUMS_FALSE_POINTS: int = -2
# Driver points
DRIVER_RACE_POINTS: Dict[int, int] = { DRIVER_RACE_POINTS: Dict[int, int] = {
1: 25, 1: 25,
2: 18, 2: 18,
@ -45,19 +42,6 @@ DRIVER_RACE_POINTS: Dict[int, int] = {
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_FASTEST_LAP_POINTS: int = 1
# Last season results
WDC_STANDING_2023: Dict[str, int] = { WDC_STANDING_2023: Dict[str, int] = {
"Max Verstappen": 1, "Max Verstappen": 1,
@ -81,6 +65,7 @@ WDC_STANDING_2023: Dict[str, int] = {
"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,
@ -121,7 +106,6 @@ class PointsModel(Model):
_points_per_step: Dict[str, List[int]] | None = None _points_per_step: Dict[str, List[int]] | None = None
_driver_points_per_step: Dict[str, List[int]] | None = None _driver_points_per_step: Dict[str, List[int]] | None = None
_active_driver_points_per_step: Dict[str, List[int]] | None = None
_team_points_per_step: Dict[str, List[int]] | None = None _team_points_per_step: Dict[str, List[int]] | None = None
_dnfs: Dict[str, int] | None = None _dnfs: Dict[str, int] | None = None
@ -149,50 +133,26 @@ class PointsModel(Model):
return self._points_per_step return self._points_per_step
def driver_points_per_step(self, *, include_inactive: bool) -> Dict[str, List[int]]: # @todo Doesn't include fastest lap + sprint points
def driver_points_per_step(self) -> 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.
""" """
if include_inactive: if self._driver_points_per_step is None:
if self._driver_points_per_step is None: self._driver_points_per_step = dict()
self._driver_points_per_step = dict() for driver in self.all_drivers(include_none=False):
for driver in self.all_drivers(include_none=False, include_inactive=True): self._driver_points_per_step[driver.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers
self._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():
for position, driver in race_result.standing.items():
driver_name: str = driver.name
race_number: int = race_result.race.number race_number: int = race_result.race.number
for position, driver in race_result.standing.items(): self._driver_points_per_step[driver_name][race_number] = DRIVER_RACE_POINTS[int(position)] if int(position) in DRIVER_RACE_POINTS else 0
self._driver_points_per_step[driver.name][race_number] = DRIVER_RACE_POINTS[int(position)] if int(position) in DRIVER_RACE_POINTS else 0
self._driver_points_per_step[driver.name][race_number] += DRIVER_FASTEST_LAP_POINTS if race_result.fastest_lap_driver == driver else 0
for position, driver in race_result.sprint_standing.items():
driver_name: str = driver.name
self._driver_points_per_step[driver_name][race_number] += DRIVER_SPRINT_POINTS[int(position)] if int(position) in DRIVER_SPRINT_POINTS else 0
return self._driver_points_per_step
else:
if self._active_driver_points_per_step is None:
self._active_driver_points_per_step = dict()
for driver in self.all_drivers(include_none=False, include_inactive=False):
self._active_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():
race_number: int = race_result.race.number
for position, driver in race_result.standing.items():
self._active_driver_points_per_step[driver.name][race_number] = DRIVER_RACE_POINTS[int(position)] if int(position) in DRIVER_RACE_POINTS else 0
self._active_driver_points_per_step[driver.name][race_number] += DRIVER_FASTEST_LAP_POINTS if race_result.fastest_lap_driver == driver else 0
for position, driver in race_result.sprint_standing.items():
driver_name: str = driver.name
self._active_driver_points_per_step[driver_name][race_number] += DRIVER_SPRINT_POINTS[int(position)] if int(position) in DRIVER_SPRINT_POINTS else 0
return self._active_driver_points_per_step
return self._driver_points_per_step
# @todo Doesn't include fastest lap + sprint points
def team_points_per_step(self) -> Dict[str, List[int]]: def team_points_per_step(self) -> Dict[str, List[int]]:
""" """
Returns a dictionary of lists, containing points per race for each team. Returns a dictionary of lists, containing points per race for each team.
@ -203,28 +163,26 @@ class PointsModel(Model):
self._team_points_per_step[team.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers self._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 position, driver in race_result.standing.items():
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
self._team_points_per_step[team_name][race_number] += self.driver_points_per_step(include_inactive=True)[driver.name][race_number] self._team_points_per_step[team_name][race_number] += DRIVER_RACE_POINTS[int(position)] if int(position) in DRIVER_RACE_POINTS else 0
return self._team_points_per_step return self._team_points_per_step
# @todo Doesn't include sprint dnfs
def dnfs(self) -> Dict[str, int]: def dnfs(self) -> Dict[str, int]:
if self._dnfs is None: if self._dnfs is None:
self._dnfs = dict() self._dnfs = dict()
for driver in self.all_drivers(include_none=False, include_inactive=True): for driver in self.all_drivers(include_none=False):
self._dnfs[driver.name] = 0 self._dnfs[driver.name] = 0
for race_result in self.all_race_results(): for race_result in self.all_race_results():
for driver in race_result.all_dnfs: for driver in race_result.all_dnfs:
self._dnfs[driver.name] += 1 self._dnfs[driver.name] += 1
for driver in race_result.sprint_dnfs:
self._dnfs[driver.name] += 1
return self._dnfs return self._dnfs
# #
@ -236,41 +194,41 @@ class PointsModel(Model):
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().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) -> 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)
@overload @overload
def driver_points_by(self, *, race_name: str, include_inactive: bool) -> Dict[str, int]: def driver_points_by(self, *, race_name: str) -> 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)
@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) -> 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)
def driver_points_by(self, *, driver_name: str | None = None, race_name: str | None = None, include_inactive: bool) -> List[int] | Dict[str, int] | int: def driver_points_by(self, *, driver_name: str | None = None, race_name: str | None = None) -> 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()[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().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
@ -278,27 +236,27 @@ 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()[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")
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))
def drivers_sorted_by_points(self, *, include_inactive: bool) -> List[Driver]: def drivers_sorted_by_points(self) -> List[Driver]:
comparator: Callable[[Driver], int] = lambda driver: self.total_driver_points_by(driver.name) comparator: Callable[[Driver], int] = 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) return sorted(self.all_drivers(include_none=False), key=comparator, reverse=True)
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): for position in range(1, len(self.all_drivers(include_none=False)) + 1):
standing[position] = list() 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():
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 position += 1
@ -314,7 +272,7 @@ class PointsModel(Model):
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():
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 position += 1
@ -325,9 +283,6 @@ class PointsModel(Model):
return standing return standing
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:
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]
def most_dnf_names(self) -> List[str]: def most_dnf_names(self) -> List[str]:
@ -348,13 +303,13 @@ class PointsModel(Model):
most_gained_names: List[str] = list() most_gained_names: List[str] = list()
most_gained: int = 0 most_gained: int = 0
for driver in self.all_drivers(include_none=False, include_inactive=True): for driver in self.all_drivers(include_none=False):
gained: int = self.wdc_diff_2023_by(driver.name) gained: int = self.wdc_diff_2023_by(driver.name)
if gained > most_gained: if gained > most_gained:
most_gained = gained most_gained = gained
for driver in self.all_drivers(include_none=False, include_inactive=True): for driver in self.all_drivers(include_none=False):
gained: int = self.wdc_diff_2023_by(driver.name) gained: int = self.wdc_diff_2023_by(driver.name)
if gained == most_gained: if gained == most_gained:
@ -366,13 +321,13 @@ class PointsModel(Model):
most_lost_names: List[str] = list() most_lost_names: List[str] = list()
most_lost: int = 100 most_lost: int = 100
for driver in self.all_drivers(include_none=False, include_inactive=True): for driver in self.all_drivers(include_none=False):
lost: int = self.wdc_diff_2023_by(driver.name) lost: int = self.wdc_diff_2023_by(driver.name)
if lost < most_lost: if lost < most_lost:
most_lost = lost most_lost = lost
for driver in self.all_drivers(include_none=False, include_inactive=True): for driver in self.all_drivers(include_none=False):
lost: int = self.wdc_diff_2023_by(driver.name) lost: int = self.wdc_diff_2023_by(driver.name)
if lost == most_lost: if lost == most_lost:
@ -395,8 +350,8 @@ class PointsModel(Model):
return points_per_step_cumulative return points_per_step_cumulative
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(team_name=team_name)
return sum(sum(self.driver_points_by(driver_name=teammate.name, include_inactive=True)) for teammate in teammates) return sum(self.driver_points_by(driver_name=teammates[0].name)) + sum(self.driver_points_by(driver_name=teammates[1].name))
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)
@ -595,7 +550,7 @@ 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()
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(team_name=driver.team.name)
teammate: Driver = teammates[0] if teammates[1] == driver else teammates[1] teammate: Driver = teammates[0] if teammates[1] == driver else teammates[1]
print(f"{driver.name} standing: {self.wdc_standing_by_driver()[driver.name]}, {teammate.name} standing: {self.wdc_standing_by_driver()[teammate.name]}") print(f"{driver.name} standing: {self.wdc_standing_by_driver()[driver.name]}, {teammate.name} standing: {self.wdc_standing_by_driver()[teammate.name]}")
@ -645,7 +600,7 @@ class PointsModel(Model):
"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)
] ]
return json.dumps(data) return json.dumps(data)

View File

@ -29,12 +29,10 @@ class TemplateModel(Model):
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) self.active_result = self.race_result_by(race_name=active_result_race_name)
@staticmethod def race_guess_open(self, race: Race) -> bool:
def race_guess_open(race: Race) -> bool:
return not race_has_started(race=race) if ENABLE_TIMING else True return not race_has_started(race=race) if ENABLE_TIMING else True
@staticmethod def season_guess_open(self) -> bool:
def season_guess_open() -> bool:
return not race_has_started(race_id=1) if ENABLE_TIMING else True return not race_has_started(race_id=1) if ENABLE_TIMING else True
def race_result_open(self, race_name: str) -> bool: def race_result_open(self, race_name: str) -> bool:
@ -71,14 +69,6 @@ class TemplateModel(Model):
def current_race(self) -> Race | None: def current_race(self) -> Race | None:
return self.first_race_without_result() return self.first_race_without_result()
def active_result_race_or_current_race(self) -> Race:
if self.active_result is not None:
return self.active_result.race
elif self.current_race is not None:
return self.current_race
else:
return self.all_races()[0]
def active_result_race_name_or_current_race_name(self) -> str: def active_result_race_name_or_current_race_name(self) -> str:
if self.active_result is not None: if self.active_result is not None:
return self.active_result.race.name return self.active_result.race.name
@ -95,12 +85,9 @@ class TemplateModel(Model):
else: else:
return self.all_races()[0].name_sanitized return self.all_races()[0].name_sanitized
def all_active_drivers_or_active_result_standing_drivers(self) -> List[Driver]: def all_drivers_or_active_result_standing_drivers(self) -> List[Driver]:
return self.active_result.ordered_standing_list() if self.active_result is not None else self.all_drivers(include_none=False, include_inactive=False) return self.active_result.ordered_standing_list() if self.active_result is not None else self.all_drivers(include_none=False)
def all_active_drivers_or_active_result_sprint_standing_drivers(self) -> List[Driver]: def drivers_for_wdc_gained(self) -> List[Driver]:
return self.active_result.ordered_sprint_standing_list() if self.active_result is not None else self.all_drivers(include_none=False, include_inactive=False)
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))

View File

@ -1,55 +0,0 @@
from typing import Any, Callable, Dict
class ApiDriver:
__type_conversion_map__: Dict[str, Callable[[Any], Any]] = {
"session_key": int,
"meeting_key": int,
"full_name": str,
"first_name": str,
"last_name": str,
"name_acronym": str,
"broadcast_name": str,
"country_code": str,
"headshot_url": str,
"driver_number": int,
"team_colour": str,
"team_name": str
}
def __init__(self, response: dict[str, str] | None):
if response is None:
return
for key in response:
if not hasattr(self, key):
raise Exception(f"Mismatch between response data and {type(self).__name__} (key={key})")
if not key in self.__type_conversion_map__:
raise Exception(f"Mismatch between response data and {type(self).__name__}.__type_map__ (key={key})")
setattr(self, key, self.__type_conversion_map__[key](response[key]))
print("ApiDriver:", self.__dict__)
def to_params(self) -> Dict[str, str]:
params: Dict[str, str] = dict()
for key in self.__dict__:
params[str(key)] = str(self.__dict__[key])
return params
# Set all members to None so hasattr works above
session_key: int = None # type: ignore
meeting_key: int = None # type: ignore
full_name: str = None # type: ignore
first_name: str = None # type: ignore
last_name: str = None # type: ignore
name_acronym: str = None # type: ignore
broadcast_name: str = None # type: ignore
country_code: str = None # type: ignore
headshot_url: str = None # type: ignore
driver_number: int = None # type: ignore
team_colour: str = None # type: ignore
team_name: str = None # type: ignore

View File

@ -1,40 +0,0 @@
from datetime import datetime
from typing import Any, Callable, Dict
class ApiPosition:
__type_conversion_map__: Dict[str, Callable[[Any], Any]] = {
"session_key": int,
"meeting_key": int,
"driver_number": int,
"date": lambda date: datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%f"),
"position": int
}
def __init__(self, response: dict[str, str] | None):
if response is None:
return
for key in response:
if not hasattr(self, key):
raise Exception(f"Mismatch between response data and {type(self).__name__} (key={key})")
if not key in self.__type_conversion_map__:
raise Exception(f"Mismatch between response data and {type(self).__name__}.__type_map__ (key={key})")
setattr(self, key, self.__type_conversion_map__[key](response[key]))
print("ApiPosition:", self.__dict__)
def to_params(self) -> Dict[str, str]:
params: Dict[str, str] = dict()
for key in self.__dict__:
params[str(key)] = str(self.__dict__[key])
return params
session_key: int = None # type: ignore
meeting_key: int = None # type: ignore
driver_number: int = None # type: ignore
date: datetime = None # type: ignore
position: int = None # type: ignore

View File

@ -1,60 +0,0 @@
from datetime import datetime, time
from typing import Any, Callable, Dict
class ApiSession:
__type_conversion_map__: Dict[str, Callable[[Any], Any]] = {
"location": str,
"country_key": int,
"country_code": str,
"country_name": str,
"circuit_key": int,
"circuit_short_name": str,
"session_type": str,
"session_name": str,
"date_start": lambda date: datetime.strptime(date, "%Y-%m-%dT%H:%M:%S"),
"date_end": lambda date: datetime.strptime(date, "%Y-%m-%dT%H:%M:%S"),
"gmt_offset": lambda time: datetime.strptime(time, "%H:%M:%S").time(),
"session_key": int,
"meeting_key": int,
"year": int
}
def __init__(self, response: dict[str, str] | None):
if response is None:
return
for key in response:
if not hasattr(self, key):
raise Exception(f"Mismatch between response data and {type(self).__name__} (key={key})")
if not key in self.__type_conversion_map__:
raise Exception(f"Mismatch between response data and {type(self).__name__}.__type_map__ (key={key})")
setattr(self, key, self.__type_conversion_map__[key](response[key]))
print("ApiSession:", self.__dict__)
def to_params(self) -> Dict[str, str]:
params: Dict[str, str] = dict()
for key in self.__dict__:
params[str(key)] = str(self.__dict__[key])
return params
location: str = None # type: ignore
country_key: int = None # type: ignore
country_code: str = None # type: ignore
country_name: str = None # type: ignore
circuit_key: int = None # type: ignore
circuit_short_name: str = None # type: ignore
session_type: str = None # type: ignore
session_name: str = None # type: ignore
date_start: datetime = None # type: ignore
date_end: datetime = None # type: ignore
gmt_offset: time = None # type: ignore
session_key: int = None # type: ignore
meeting_key: int = None # type: ignore
year: int = None # type: ignore

View File

@ -1,9 +0,0 @@
OPENF1_URL: str = "https://api.openf1.org/v1"
OPENF1_SESSION_ENDPOINT: str = f"{OPENF1_URL}/sessions"
OPENF1_POSITION_ENDPOINT: str = f"{OPENF1_URL}/position"
OPENF1_DRIVER_ENDPOINT: str = f"{OPENF1_URL}/drivers"
OPENF1_SESSION_TYPE_RACE: str = "Race"
OPENF1_SESSION_NAME_RACE: str = "Race"
OPENF1_SESSION_NAME_SPRINT: str = "Sprint"

View File

@ -1,76 +0,0 @@
from datetime import datetime
import json
from typing import Any, Callable, Dict, List, cast
from requests import Response, get
from formula10.openf1.model.api_driver import ApiDriver
from formula10.openf1.model.api_position import ApiPosition
from formula10.openf1.model.api_session import ApiSession
from formula10.openf1.openf1_definitions import OPENF1_DRIVER_ENDPOINT, OPENF1_POSITION_ENDPOINT, OPENF1_SESSION_ENDPOINT, OPENF1_SESSION_NAME_RACE, OPENF1_SESSION_NAME_SPRINT, OPENF1_SESSION_TYPE_RACE
def request_helper(endpoint: str, params: Dict[str, str]) -> List[Dict[str, str]]:
response: Response = get(endpoint, params=params)
if not response.ok:
raise Exception(f"OpenF1 request to {response.request.url} failed")
obj: Any = json.loads(response.text)
if isinstance(obj, List):
return cast(List[Dict[str, str]], obj)
elif isinstance(obj, Dict):
return [cast(Dict[str, str], obj)]
else:
# @todo Fail gracefully
raise Exception(f"Unexpected OpenF1 response from {response.request.url}: {obj}")
def fetch_openf1_latest_session(session_name: str) -> ApiSession:
# ApiSession object only supports integer session_keys
response: List[Dict[str, str]] = request_helper(OPENF1_SESSION_ENDPOINT, {
"session_key": "latest",
"session_type": OPENF1_SESSION_TYPE_RACE,
"session_name": session_name
})
return ApiSession(response[0])
def fetch_openf1_latest_race_session_key() -> int:
return fetch_openf1_latest_session(OPENF1_SESSION_NAME_RACE).session_key
def fetch_openf1_latest_sprint_session_key() -> int:
return fetch_openf1_latest_session(OPENF1_SESSION_NAME_SPRINT).session_key
def fetch_openf1_session(session_name: str, country_code: str) -> ApiSession:
_session: ApiSession = ApiSession(None)
_session.session_type = OPENF1_SESSION_TYPE_RACE # includes races + sprints
_session.year = 2024
_session.country_code = country_code
_session.session_name = session_name
response: List[Dict[str, str]] = request_helper(OPENF1_SESSION_ENDPOINT, _session.to_params())
return ApiSession(response[0])
def fetch_openf1_driver(session_key: int, name_acronym: str) -> ApiDriver:
_driver: ApiDriver = ApiDriver(None)
_driver.name_acronym = name_acronym
_driver.session_key = session_key
response: List[Dict[str, str]] = request_helper(OPENF1_DRIVER_ENDPOINT, _driver.to_params())
return ApiDriver(response[0])
def fetch_openf1_position(session_key: int, position: int):
_position: ApiPosition = ApiPosition(None)
_position.session_key = session_key
_position.position = position
response: List[Dict[str, str]] = request_helper(OPENF1_POSITION_ENDPOINT, _position.to_params())
# Find the last driver that was on this position at last
predicate: Callable[[Dict[str, str]], datetime] = lambda position: datetime.strptime(position["date"], "%Y-%m-%dT%H:%M:%S.%f")
return ApiPosition(max(response, key=predicate))

View File

@ -26,11 +26,12 @@
{# 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, 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) %}
{% endif %} {% endif %}
{% for driver in drivers %} {% for driver in drivers %}
@ -44,12 +45,13 @@
{# 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, 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) %}
{% endif %} {% endif %}
{% for driver in drivers %} {% for driver in drivers %}
@ -77,7 +79,8 @@
{# Simple team select for forms #} {# Simple team select for forms #}
{% macro team_select(name, label, include_none, teams=none, disabled=false, border="") %} {% macro team_select(name, label, include_none, teams=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 teams == none %} {% if teams == none %}
@ -95,7 +98,8 @@
{# Team select for forms where a value might be preselected #} {# Team select for forms where a value might be preselected #}
{% macro team_select_with_preselect(team_match, name, label, include_none, teams=none, disabled=false, border="") %} {% macro team_select_with_preselect(team_match, name, label, include_none, teams=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(teampre=false) %} {% set user_has_chosen = namespace(teampre=false) %}
@ -218,7 +222,7 @@
<div class="flex-grow-1"></div> <div class="flex-grow-1"></div>
<div class="navbar-nav"> <div class="navbar-nav">
{{ nav_selector(page="/result", text="Enter Race Result") }} {# {{ nav_selector(page="/result", text="Enter Race Result") }} #}
{{ nav_selector(page="/user", text="Manage Users") }} {{ nav_selector(page="/user", text="Manage Users") }}
</div> </div>
</div> </div>

View File

@ -42,176 +42,90 @@
{% block body %} {% block body %}
{% set race_result_open=model.race_result_open(model.active_result_race_name_or_current_race_name()) %} <div class="grid card-grid">
{% if race_result_open == true %}
{% set action_save_href = "/result-enter/" ~ model.active_result_race_name_or_current_race_name_sanitized() %}
{% set action_fetch_href = "/result-fetch/" ~ model.active_result_race_name_or_current_race_name_sanitized() %}
{% else %}
{% set action_save_href = "" %}
{% set action_fetch_href = "" %}
{% endif %}
<form action="{{ action_fetch_href }}" method="post"> <div class="card shadow-sm mb-2" style="max-width: 450px;">
<div class="card shadow-sm mb-2 w-100">
<div class="card-header">
OpenF1
</div>
<div class="card-body">
<input type="submit" class="btn btn-danger mt-2 w-100" value="Fetch from OpenF1"
{% if race_result_open == false %}disabled="disabled"{% endif %}>
</div>
</div>
</form>
<form class="grid card-grid" action="{{ action_save_href }}" method="post">
{# Race result #}
<div class="card shadow-sm mb-2 w-100">
<div class="card-header"> <div class="card-header">
{{ model.active_result_race_name_or_current_race_name() }} {{ model.active_result_race_name_or_current_race_name() }}
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="d-inline-block overflow-x-scroll w-100"> <div class="d-inline-block overflow-x-scroll w-100">
<div style="width: 460px;"> <div style="width: 410px;">
{% set race_result_open=model.race_result_open(model.active_result_race_name_or_current_race_name()) %}
{# Place numbers #} {% if race_result_open == true %}
<ul class="list-group list-group-flush d-inline-block"> {% set action_save_href = "/result-enter/" ~ model.active_result_race_name_or_current_race_name_sanitized() %}
{% for driver in model.all_active_drivers_or_active_result_standing_drivers() %} {% else %}
<li class="list-group-item p-1"><span id="place_number" {% set action_save_href = "" %}
class="fw-bold">P{{ "%02d" % loop.index }}</span>: {% endif %}
</li>
{% endfor %}
</ul>
{# Drag and drop, "#columns .column" is the selector for the JS #}
<ul id="columns" class="list-group list-group-flush d-inline-block float-end">
{% for driver in model.all_active_drivers_or_active_result_standing_drivers() %}
<li class="list-group-item {% if race_result_open == true %}column{% endif %} p-1"
{% if race_result_open == true %}draggable="true"{% endif %}>
{{ driver.name }}
<div class="d-inline-block float-end" style="margin-left: 30px;">
{# Fastest lap #}
<div class="form-check form-check-reverse d-inline-block">
<input type="radio" class="form-check-input"
value="{{ driver.id }}"
id="fastest-lap-{{ driver.id }}" name="fastest-lap"
{% if (model.active_result is not none) and (driver.id == model.active_result.fastest_lap_driver.id) %}checked{% endif %}
{% if race_result_open == false %}disabled="disabled"{% endif %}>
<label for="fastest-lap-{{ driver.id }}"
class="form-check-label text-muted" data-bs-toggle="tooltip"
title="Fastest lap">Lap</label>
</div>
{# Driver DNFed at first #}
<div class="form-check form-check-reverse d-inline-block"
style="margin-left: 2px;">
<input type="checkbox" class="form-check-input"
value="{{ driver.id }}"
id="first-dnf-{{ driver.id }}" name="first-dnf-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.initial_dnf) %}checked{% endif %}
{% if race_result_open == false %}disabled="disabled"{% endif %}>
<label for="first-dnf-{{ driver.id }}"
class="form-check-label text-muted">1. DNF</label>
</div>
{# Driver DNFed #}
<div class="form-check form-check-reverse d-inline-block"
style="margin-left: 2px;">
<input type="checkbox" class="form-check-input"
value="{{ driver.id }}"
id="dnf-{{ driver.id }}" name="dnf-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.all_dnfs) %}checked{% endif %}
{% if race_result_open == false %}disabled="disabled"{% endif %}>
<label for="dnf-{{ driver.id }}"
class="form-check-label text-muted">DNF</label>
</div>
{# Driver Excluded #}
<div class="form-check form-check-reverse d-inline-block"
style="margin-left: 2px;">
<input type="checkbox" class="form-check-input"
value="{{ driver.id }}"
id="exclude-{{ driver.id }}" name="excluded-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.standing_exclusions) %}checked{% endif %}
{% if race_result_open == false %}disabled="disabled"{% endif %}>
<label for="exclude-{{ driver.id }}"
class="form-check-label text-muted" data-bs-toggle="tooltip"
title="Driver is not counted for standing">NC</label>
</div>
</div>
{# Standing order #}
<input type="hidden" name="pxx-drivers" value="{{ driver.id }}">
</li>
{% endfor %}
</ul>
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save"
{% if race_result_open == false %}disabled="disabled"{% endif %}>
</div>
</div>
</div>
</div>
{# Sprint result #}
{% if model.active_result_race_or_current_race().has_sprint == true %}
<div class="card shadow-sm mb-2 w-100">
<div class="card-header">
Sprint
</div>
<div class="card-body">
<div class="d-inline-block overflow-x-scroll w-100">
<div style="width: 275px;">
<form action="{{ action_save_href }}" method="post">
{# Place numbers #} {# Place numbers #}
<ul class="list-group list-group-flush d-inline-block"> <ul class="list-group list-group-flush d-inline-block">
{% for driver in model.all_active_drivers_or_active_result_sprint_standing_drivers() %} {% for driver in model.all_drivers_or_active_result_standing_drivers() %}
<li class="list-group-item p-1"><span id="place_number" <li class="list-group-item p-1"><span id="place_number"
class="fw-bold">P{{ "%02d" % loop.index }}</span>: class="fw-bold">P{{ "%02d" % loop.index }}</span>:
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{# Drag and drop, "#columns .column" is the selector for the JS #} {# Drag and drop #}
<ul id="columns" class="list-group list-group-flush d-inline-block float-end"> <ul id="columns" class="list-group list-group-flush d-inline-block float-end">
{% for driver in model.all_active_drivers_or_active_result_sprint_standing_drivers() %}
{% for driver in model.all_drivers_or_active_result_standing_drivers() %}
<li class="list-group-item {% if race_result_open == true %}column{% endif %} p-1" <li class="list-group-item {% if race_result_open == true %}column{% endif %} p-1"
{% if race_result_open == true %}draggable="true"{% endif %}> {% if race_result_open == true %}draggable="true"{% endif %}>
{{ driver.name }} {{ driver.name }}
<div class="d-inline-block float-end" style="margin-left: 30px;"> <div class="d-inline-block float-end" style="margin-left: 30px;">
{# Driver DNFed at first #}
{# Driver DNFed #} <div class="form-check form-check-reverse d-inline-block">
<div class="form-check form-check-reverse d-inline-block"
style="margin-left: 2px;">
<input type="checkbox" class="form-check-input" <input type="checkbox" class="form-check-input"
value="{{ driver.id }}" value="{{ driver.id }}"
id="sprint-dnf-{{ driver.id }}" name="sprint-dnf-drivers" id="first-dnf-{{ driver.id }}" name="first-dnf-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.sprint_dnfs) %}checked{% endif %} {% if (model.active_result is not none) and (driver in model.active_result.initial_dnf) %}checked{% endif %}
{% if race_result_open == false %}disabled="disabled"{% endif %}> {% if race_result_open == false %}disabled="disabled"{% endif %}>
<label for="sprint-dnf-{{ driver.id }}" <label for="first-dnf-{{ driver.id }}"
class="form-check-label text-muted">1. DNF</label>
</div>
{# Driver DNFed #}
<div class="form-check form-check-reverse d-inline-block mx-2">
<input type="checkbox" class="form-check-input"
value="{{ driver.id }}"
id="dnf-{{ driver.id }}" name="dnf-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.all_dnfs) %}checked{% endif %}
{% if race_result_open == false %}disabled="disabled"{% endif %}>
<label for="dnf-{{ driver.id }}"
class="form-check-label text-muted">DNF</label> class="form-check-label text-muted">DNF</label>
</div> </div>
{# Driver Excluded #}
<div class="form-check form-check-reverse d-inline-block">
<input type="checkbox" class="form-check-input"
value="{{ driver.id }}"
id="exclude-{{ driver.id }}" name="excluded-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.standing_exclusions) %}checked{% endif %}
{% if race_result_open == false %}disabled="disabled"{% endif %}>
<label for="exclude-{{ driver.id }}"
class="form-check-label text-muted" data-bs-toggle="tooltip"
title="Driver is not counted for standing">NC</label>
</div>
</div> </div>
{# Standing order #} {# Standing order #}
<input type="hidden" name="sprint-pxx-drivers" value="{{ driver.id }}"> <input type="hidden" name="pxx-drivers" value="{{ driver.id }}">
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save"
</div> {% if race_result_open == false %}disabled="disabled"{% endif %}>
</form>
</div> </div>
</div> </div>
</div> </div>
{% endif %} </div>
</div>
</form>
{% endblock body %} {% endblock body %}

View File

@ -18,6 +18,7 @@
<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>
Points from sprints and fastest laps are not tracked currently.
</div> </div>
</div> </div>
@ -81,7 +82,7 @@
<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, drivers=model.drivers_for_wdc_gained(),
disabled=not season_guess_open, 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 "")) }}
{{ 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",
@ -95,8 +96,8 @@
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] %} {% set driver_a = model.drivers_by(team_name=team.name)[0] %}
{% set driver_b = model.drivers_by(team_name=team.name, include_inactive=False)[1] %} {% set driver_b = model.drivers_by(team_name=team.name)[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">
@ -131,8 +132,8 @@
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] %} {% set driver_a = model.drivers_by(team_name=team.name)[0] %}
{% set driver_b = model.drivers_by(team_name=team.name, include_inactive=False)[1] %} {% set driver_b = model.drivers_by(team_name=team.name)[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">

View File

@ -6,6 +6,16 @@
{% block body %} {% block body %}
<div class="card shadow-sm mb-2">
<div class="card-header">
Note
</div>
<div class="card-body">
Points from sprints and fastest laps are not tracked currently.
</div>
</div>
<div class="grid card-grid-2"> <div class="grid card-grid-2">
<div class="card shadow-sm mb-2"> <div class="card shadow-sm mb-2">
@ -27,7 +37,7 @@
</thead> </thead>
<tbody> <tbody>
{% for driver in points.drivers_sorted_by_points(include_inactive=True) %} {% for driver in points.drivers_sorted_by_points() %}
{% set driver_standing = points.wdc_standing_by_driver()[driver.name] %} {% set driver_standing = points.wdc_standing_by_driver()[driver.name] %}
<tr class="{% if driver_standing == 1 %}table-danger{% endif %}"> <tr class="{% if driver_standing == 1 %}table-danger{% endif %}">
<td class="text-center text-nowrap">{{ driver_standing }}</td> <td class="text-center text-nowrap">{{ driver_standing }}</td>

View File

@ -4,6 +4,5 @@ numpy
flask flask
flask-sqlalchemy flask-sqlalchemy
sqlalchemy sqlalchemy
requests
pytest pytest