Compare commits

...

8 Commits

Author SHA1 Message Date
f1090f205b Add initial leaderboards page
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 38s
2024-03-02 13:11:25 +01:00
efcdf5412b Update race table styling 2024-03-02 13:11:16 +01:00
e0e6ec6bd5 Disable more elements when form is closed 2024-03-02 12:48:34 +01:00
abea8aa0c8 Allow disabling timing constraints for development 2024-03-02 12:38:12 +01:00
cf0dc88284 Add initial (untested) race guess points calculation 2024-02-27 21:01:09 +01:00
70fae278a8 Add pytest 2024-02-27 21:00:59 +01:00
262bcc8d5e Add seasonguessresult table 2024-02-27 20:21:37 +01:00
44549f019d Split base model from template + points model 2024-02-27 19:42:21 +01:00
35 changed files with 643 additions and 230 deletions

View File

@ -0,0 +1 @@
user_name,hot_take_correct,overtakes_correct
1 user_name hot_take_correct overtakes_correct

View File

@ -24,6 +24,7 @@
flask-sqlalchemy flask-sqlalchemy
sqlalchemy sqlalchemy
pytest
]); ]);
in { in {
devShell = pkgs.devshell.mkShell { devShell = pkgs.devshell.mkShell {

View File

@ -1,6 +1,13 @@
import os
from flask import Flask from flask import Flask
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
# Load local ENV variables (can be set when calling the executable)
ENABLE_TIMING: bool = False if os.getenv("DISABLE_TIMING") == "True" else True
print("Running Formula10 with:")
if not ENABLE_TIMING:
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
@ -17,19 +24,23 @@ db.init_app(app)
# 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 # type: ignore
import formula10.controller.season_controller import formula10.controller.season_controller
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 # TODO
# General
# Statistics # Statistics
# - Auto calculate points # - Auto calculate points
# - Display points somewhere in race table? # - Display points somewhere in race table? Below the name in the table header.
# - Highlight currently correct values for some season guesses (e.g. current most dnfs) # - Highlight currently correct values for some season guesses (e.g. current most dnfs, team winners, podiums)
# - Generate static diagram using chart.js + templating the js (funny yikes) # - Generate static diagram using chart.js + templating the js (funny yikes)
# - Which driver was voted most for dnf?
# General
# - Decouple names from IDs + Fix Valtteri/Russel spelling errors
# - Unit testing (as much as possible, but especially points calculation)
# Possible but probably not # Possible but probably not
# - Show cards of previous race results, like with season guesses? # - Show cards of previous race results, like with season guesses?

View File

@ -4,8 +4,8 @@ from flask import redirect, render_template, request
from werkzeug import Response from werkzeug import Response
from formula10.database.update_queries import update_race_result, update_user from formula10.database.update_queries import update_race_result, update_user
from formula10.database.import_export import export_dynamic_data, reload_static_data from formula10.database.import_export import export_dynamic_data, reload_season_guess_result_data, reload_static_data
from formula10.frontend.template_model import TemplateModel from formula10.domain.template_model import TemplateModel
from formula10 import app from formula10 import app
@ -28,6 +28,12 @@ def load_static() -> Response:
return redirect("/") return redirect("/")
@app.route("/load/seasonresults")
def load_season_results() -> Response:
reload_season_guess_result_data()
return redirect("/")
# @app.route("/load/dynamic") # @app.route("/load/dynamic")
# def load_dynamic() -> Response: # def load_dynamic() -> Response:
# reload_dynamic_data() # reload_dynamic_data()

View File

@ -2,7 +2,7 @@ from typing import cast
from flask import redirect, render_template, session from flask import redirect, render_template, session
from werkzeug import Response from werkzeug import Response
from formula10.frontend.template_model import TemplateModel from formula10.domain.template_model import TemplateModel
from formula10 import app from formula10 import app
def error_redirect(error_message: str) -> Response: def error_redirect(error_message: str) -> Response:

View File

@ -3,7 +3,7 @@ from flask import redirect, render_template, request
from werkzeug import Response from werkzeug import Response
from formula10.database.update_queries import delete_race_guess, update_race_guess from formula10.database.update_queries import delete_race_guess, update_race_guess
from formula10.frontend.template_model import TemplateModel from formula10.domain.template_model import TemplateModel
from formula10 import app from formula10 import app

View File

@ -1,7 +1,7 @@
from flask import render_template from flask import render_template
from formula10 import app from formula10 import app
from formula10.frontend.template_model import TemplateModel from formula10.domain.template_model import TemplateModel
@app.route("/rules") @app.route("/rules")
def rules_root() -> str: def rules_root() -> str:

View File

@ -5,8 +5,8 @@ from werkzeug import Response
from formula10.database.model.db_team import DbTeam from formula10.database.model.db_team import DbTeam
from formula10.database.update_queries import update_season_guess from formula10.database.update_queries import update_season_guess
from formula10.frontend.model.team import NONE_TEAM from formula10.domain.model.team import NONE_TEAM
from formula10.frontend.template_model import TemplateModel from formula10.domain.template_model import TemplateModel
from formula10 import app, db from formula10 import app, db

View File

@ -0,0 +1,11 @@
from flask import render_template
from formula10 import app
from formula10.domain.points_model import PointsModel
from formula10.domain.template_model import TemplateModel
@app.route("/graphs")
def graphs_root() -> str:
model = TemplateModel(active_user_name=None, active_result_race_name=None)
points = PointsModel()
return render_template("statistics.jinja", model=model, points=points)

View File

View File

@ -8,6 +8,7 @@ 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_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_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
@ -37,8 +38,8 @@ def write_csv(filename: str, objects: List[Any]):
# Reload static database data, this has to be called from the app context # Reload static database data, this has to be called from the app context
def reload_static_data(): def reload_static_data():
print("Initializing Database with Static Values...") print("Initializing database with static values...")
# Create it (if it doesn't exist!) # Create it/update tables (if it/they doesn't exist!)
db.create_all() db.create_all()
# Clear static data # Clear static data
@ -58,8 +59,8 @@ def reload_static_data():
def reload_dynamic_data(): def reload_dynamic_data():
print("Initializing Database with Dynamic Values...") print("Initializing database with dynamic values...")
# Create it (if it doesn't exist!) # Create it/update tables (if it/they doesn't exist!)
db.create_all() db.create_all()
# Clear dynamic data # Clear dynamic data
@ -81,6 +82,21 @@ def reload_dynamic_data():
db.session.commit() db.session.commit()
def reload_season_guess_result_data():
print("Loading season guess results...")
# Create it/update tables (if it/they doesn't exist!)
db.create_all()
# Clear result data
db.session.query(DbSeasonGuessResult).delete()
# Reload result data
for row in load_csv("data/static_import/season_guess_results.csv"):
db.session.add(DbSeasonGuessResult.from_csv(row))
db.session.commit()
def export_dynamic_data(): def export_dynamic_data():
print("Exporting Userdata...") print("Exporting Userdata...")

View File

View File

@ -0,0 +1,44 @@
from typing import List
from sqlalchemy import Boolean, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from formula10 import db
from formula10.database.model.db_user import DbUser
class DbSeasonGuessResult(db.Model):
"""
Manually entered results for the season bonus guesses.
"""
__tablename__ = "seasonguessresult"
__csv_header__ = ["user_name", "hot_take_correct", "overtakes_correct"]
def __init__(self, *, user_name: str, hot_take_correct: bool, overtakes_correct: bool):
self.user_name = user_name # Primary key
self.hot_take_correct = hot_take_correct
self.overtakes_correct = overtakes_correct
@classmethod
def from_csv(cls, row: List[str]):
db_season_guess_result: DbSeasonGuessResult = cls(user_name=str(row[0]),
hot_take_correct=True if str(row[1])=="True" else False,
overtakes_correct=True if str(row[2])=="True" else False)
return db_season_guess_result
# This object can't be edited from the page context
# def to_csv(self) -> List[Any]:
# return [
# self.user_name,
# self.hot_take_correct,
# self.overtakes_correct
# ]
user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True)
hot_take_correct: Mapped[bool] = mapped_column(Boolean)
overtakes_correct: Mapped[bool] = mapped_column(Boolean)
# Relationships
user: Mapped[DbUser] = relationship("DbUser", foreign_keys=[user_name])

View File

@ -19,7 +19,8 @@ class DbUser(db.Model):
@classmethod @classmethod
def from_csv(cls, row: List[str]): def from_csv(cls, row: List[str]):
db_user: DbUser = cls(name=str(row[0]), enabled=True if str(row[1])=="True" else False) db_user: DbUser = cls(name=str(row[0]),
enabled=True if str(row[1])=="True" else False)
return db_user return db_user
def to_csv(self) -> List[Any]: def to_csv(self) -> List[Any]:

View File

@ -11,7 +11,7 @@ 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, positions_are_contiguous, race_has_started
from formula10 import db from formula10 import ENABLE_TIMING, db
def find_or_create_race_guess(user_name: str, race_name: str) -> DbRaceGuess: def find_or_create_race_guess(user_name: str, race_name: str) -> DbRaceGuess:
@ -37,7 +37,7 @@ def update_race_guess(race_name: str, user_name: str, pxx_select: str | None, dn
if any_is_none(pxx_select, dnf_select): if any_is_none(pxx_select, dnf_select):
return error_redirect(f"Picks for race \"{race_name}\" were not saved, because you did not fill all the fields.") return error_redirect(f"Picks for race \"{race_name}\" were not saved, because you did not fill all the fields.")
if race_has_started(race_name=race_name): if ENABLE_TIMING and race_has_started(race_name=race_name):
return error_redirect(f"No picks for race \"{race_name}\" can be entered, as this race has already started.") return error_redirect(f"No picks for race \"{race_name}\" can be entered, as this race has already started.")
if race_has_result(race_name): if race_has_result(race_name):
@ -57,7 +57,7 @@ def update_race_guess(race_name: str, user_name: str, pxx_select: str | None, dn
def delete_race_guess(race_name: str, user_name: str) -> Response: def delete_race_guess(race_name: str, user_name: str) -> Response:
# Don't change guesses that are already over # Don't change guesses that are already over
if race_has_started(race_name=race_name): if ENABLE_TIMING and race_has_started(race_name=race_name):
return error_redirect(f"No picks for race \"{race_name}\" can be deleted, as this race has already started.") return error_redirect(f"No picks for race \"{race_name}\" can be deleted, as this race has already started.")
if race_has_result(race_name): if race_has_result(race_name):
@ -92,7 +92,7 @@ def find_or_create_season_guess(user_name: str) -> DbSeasonGuess:
def update_season_guess(user_name: str, guesses: List[str | None], team_winner_guesses: List[str | None], podium_driver_guesses: List[str]) -> Response: def update_season_guess(user_name: str, guesses: List[str | None], team_winner_guesses: List[str | None], podium_driver_guesses: List[str]) -> Response:
# Pylance marks type errors here, but those are intended. Columns are marked nullable. # Pylance marks type errors here, but those are intended. Columns are marked nullable.
if race_has_started(race_name="Bahrain"): if ENABLE_TIMING and race_has_started(race_name="Bahrain"):
return error_redirect("No season picks can be entered, as the season has already begun!") return error_redirect("No season picks can be entered, as the season has already begun!")
season_guess: DbSeasonGuess = find_or_create_season_guess(user_name) season_guess: DbSeasonGuess = find_or_create_season_guess(user_name)
@ -133,7 +133,7 @@ def find_or_create_race_result(race_name: str) -> DbRaceResult:
def update_race_result(race_name: str, pxx_driver_names_list: List[str], first_dnf_driver_names_list: List[str], dnf_driver_names_list: List[str], excluded_driver_names_list: List[str]) -> Response: def update_race_result(race_name: str, pxx_driver_names_list: List[str], first_dnf_driver_names_list: List[str], dnf_driver_names_list: List[str], excluded_driver_names_list: List[str]) -> Response:
if not race_has_started(race_name=race_name): if ENABLE_TIMING and not race_has_started(race_name=race_name):
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

View File

@ -3,7 +3,7 @@ from typing import Any, Callable, Iterable, List, TypeVar, overload
from formula10.database.model.db_race import DbRace from formula10.database.model.db_race import DbRace
from formula10 import db from formula10 import db
from formula10.frontend.model.race import Race from formula10.domain.model.race import Race
_T = TypeVar("_T") _T = TypeVar("_T")

View File

View File

@ -1,4 +1,4 @@
from typing import List, Callable, Dict, overload from typing import Callable, Dict, List, overload
from sqlalchemy import desc from sqlalchemy import desc
from formula10.database.model.db_driver import DbDriver from formula10.database.model.db_driver import DbDriver
@ -8,22 +8,18 @@ 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_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.frontend.model.driver import NONE_DRIVER, Driver from formula10.database.validation import find_multiple_strict, find_single_or_none_strict, find_single_strict
from formula10.frontend.model.race import Race from formula10.domain.model.driver import NONE_DRIVER, Driver
from formula10.frontend.model.race_guess import RaceGuess from formula10.domain.model.race import Race
from formula10.frontend.model.race_result import RaceResult from formula10.domain.model.race_guess import RaceGuess
from formula10.frontend.model.season_guess import SeasonGuess from formula10.domain.model.race_result import RaceResult
from formula10.frontend.model.team import NONE_TEAM, Team from formula10.domain.model.season_guess import SeasonGuess
from formula10.frontend.model.user import User from formula10.domain.model.team import NONE_TEAM, Team
from formula10.database.validation import find_first_else_none, find_multiple_strict, find_single_strict, find_single_or_none_strict, race_has_started from formula10.domain.model.user import User
from formula10 import db from formula10 import db
class TemplateModel: class Model():
"""
This class bundles all data required from inside a template.
"""
_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
@ -32,34 +28,9 @@ class TemplateModel:
_all_drivers: List[Driver] | None = None _all_drivers: List[Driver] | None = None
_all_teams: List[Team] | None = None _all_teams: List[Team] | None = None
active_user: User | None = None
active_result: RaceResult | None = None
# RIC is excluded, since he didn't drive as many races 2023 as the others
_wdc_gained_excluded_abbrs: List[str] = ["RIC"]
def __init__(self, *, active_user_name: str | None, active_result_race_name: str | None):
if active_user_name is not None:
self.active_user = self.user_by(user_name=active_user_name, ignore=["Everyone"])
if active_result_race_name is not None:
self.active_result = self.race_result_by(race_name=active_result_race_name)
def race_guess_open(self, race: Race) -> bool:
return not race_has_started(race=race)
def season_guess_open(self) -> bool:
return not race_has_started(race_name="Bahrain")
def active_user_name_or_everyone(self) -> str:
return self.active_user.name if self.active_user is not None else "Everyone"
def active_user_name_sanitized_or_everyone(self) -> str:
return self.active_user.name_sanitized if self.active_user is not None else "Everyone"
def all_users(self) -> List[User]: def all_users(self) -> List[User]:
""" """
Returns a list of all users in the database. Returns a list of all enabled users.
""" """
if self._all_users is None: if self._all_users is None:
self._all_users = [ self._all_users = [
@ -69,11 +40,89 @@ class TemplateModel:
return self._all_users return self._all_users
def all_users_or_active_user(self) -> List[User]: def all_race_results(self) -> List[RaceResult]:
if self.active_user is not None: """
return [self.active_user] Returns a list of all race results, in descending order (most recent first).
"""
if self._all_race_results is None:
self._all_race_results = [
RaceResult.from_db_race_result(db_race_result)
for db_race_result in db.session.query(DbRaceResult).join(DbRaceResult.race).order_by(desc(DbRace.number)).all()
]
return self.all_users() return self._all_race_results
def all_race_guesses(self) -> List[RaceGuess]:
"""
Returns a list of all race guesses (of enabled users).
"""
if self._all_race_guesses is None:
self._all_race_guesses = [
RaceGuess.from_db_race_guess(db_race_guess)
for db_race_guess in db.session.query(DbRaceGuess).join(DbRaceGuess.user).filter_by(enabled=True).all() # Ignore disabled users
]
return self._all_race_guesses
def all_season_guesses(self) -> List[SeasonGuess]:
"""
Returns a list of all season guesses (of enabled users).
"""
if self._all_season_guesses is None:
self._all_season_guesses = [
SeasonGuess.from_db_season_guess(db_season_guess)
for db_season_guess in db.session.query(DbSeasonGuess).join(DbSeasonGuess.user).filter_by(enabled=True).all() # Ignore disabled users
]
return self._all_season_guesses
def all_races(self) -> List[Race]:
"""
Returns a list of all races, in descending order (last race first).
"""
if self._all_races is None:
self._all_races = [
Race.from_db_race(db_race)
for db_race in db.session.query(DbRace).order_by(desc(DbRace.number)).all()
]
return self._all_races
def all_drivers(self, *, include_none: bool) -> List[Driver]:
"""
Returns a list of all drivers.
"""
if self._all_drivers is None:
self._all_drivers = [
Driver.from_db_driver(db_driver)
for db_driver in db.session.query(DbDriver).all()
]
if include_none:
return self._all_drivers
else:
predicate: Callable[[Driver], bool] = lambda driver: driver != NONE_DRIVER
return find_multiple_strict(predicate, self._all_drivers)
def all_teams(self, *, include_none: bool) -> List[Team]:
"""
Returns a list of all teams.
"""
if self._all_teams is None:
self._all_teams = [
Team.from_db_team(db_team)
for db_team in db.session.query(DbTeam).all()
]
if include_none:
return self._all_teams
else:
predicate: Callable[[Team], bool] = lambda team: team != NONE_TEAM
return find_multiple_strict(predicate, self._all_teams)
#
# User queries
#
@overload @overload
def user_by(self, *, user_name: str) -> User: def user_by(self, *, user_name: str) -> User:
@ -99,17 +148,9 @@ class TemplateModel:
predicate: Callable[[User], bool] = lambda user: user.name == user_name predicate: Callable[[User], bool] = lambda user: user.name == user_name
return find_single_strict(predicate, self.all_users()) return find_single_strict(predicate, self.all_users())
def all_race_results(self) -> List[RaceResult]: #
""" # Race result queries
Returns a list of all race results in the database, in descending order (most recent first). #
"""
if self._all_race_results is None:
self._all_race_results = [
RaceResult.from_db_race_result(db_race_result)
for db_race_result in db.session.query(DbRaceResult).join(DbRaceResult.race).order_by(desc(DbRace.number)).all()
]
return self._all_race_results
def race_result_by(self, *, race_name: str) -> RaceResult | None: def race_result_by(self, *, race_name: str) -> RaceResult | None:
""" """
@ -118,17 +159,9 @@ class TemplateModel:
predicate: Callable[[RaceResult], bool] = lambda result: result.race.name == race_name predicate: Callable[[RaceResult], bool] = lambda result: result.race.name == race_name
return find_single_or_none_strict(predicate, self.all_race_results()) return find_single_or_none_strict(predicate, self.all_race_results())
def all_race_guesses(self) -> List[RaceGuess]: #
""" # Race guess queries
Returns a list of all race guesses in the database. #
"""
if self._all_race_guesses is None:
self._all_race_guesses = [
RaceGuess.from_db_race_guess(db_race_guess)
for db_race_guess in db.session.query(DbRaceGuess).join(DbRaceGuess.user).filter_by(enabled=True).all() # Ignore disabled users
]
return self._all_race_guesses
@overload @overload
def race_guesses_by(self, *, user_name: str) -> List[RaceGuess]: def race_guesses_by(self, *, user_name: str) -> List[RaceGuess]:
@ -189,14 +222,9 @@ class TemplateModel:
raise Exception("race_guesses_by encountered illegal combination of arguments") raise Exception("race_guesses_by encountered illegal combination of arguments")
def all_season_guesses(self) -> List[SeasonGuess]: #
if self._all_season_guesses is None: # Season guess queries
self._all_season_guesses = [ #
SeasonGuess.from_db_season_guess(db_season_guess)
for db_season_guess in db.session.query(DbSeasonGuess).join(DbSeasonGuess.user).filter_by(enabled=True).all() # Ignore disabled users
]
return self._all_season_guesses
@overload @overload
def season_guesses_by(self, *, user_name: str) -> SeasonGuess: def season_guesses_by(self, *, user_name: str) -> SeasonGuess:
@ -228,92 +256,16 @@ class TemplateModel:
raise Exception("season_guesses_by encountered illegal combination of arguments") raise Exception("season_guesses_by encountered illegal combination of arguments")
def all_races(self) -> List[Race]: #
""" # Team queries
Returns a list of all races in the database. #
"""
if self._all_races is None:
self._all_races = [
Race.from_db_race(db_race)
for db_race in db.session.query(DbRace).order_by(desc(DbRace.number)).all()
]
return self._all_races
def first_race_without_result(self) -> Race | None:
"""
Returns the first race-object with no associated race result.
"""
results: List[RaceResult] = self.all_race_results()
if len(results) == 0:
return self.all_races()[-1] # all_races is sorted descending by number
most_recent_result: RaceResult = results[0]
predicate: Callable[[Race], bool] = lambda race: race.number == most_recent_result.race.number + 1
return find_first_else_none(predicate, self.all_races())
@property
def current_race(self) -> Race | None:
return self.first_race_without_result()
def active_result_race_name_or_current_race_name(self) -> str:
if self.active_result is not None:
return self.active_result.race.name
elif self.current_race is not None:
return self.current_race.name
else:
return self.all_races()[0].name
def active_result_race_name_or_current_race_name_sanitized(self) -> str:
if self.active_result is not None:
return self.active_result.race.name_sanitized
elif self.current_race is not None:
return self.current_race.name_sanitized
else:
return self.all_races()[0].name_sanitized
def all_teams(self, *, include_none: bool) -> List[Team]:
"""
Returns a list of all teams in the database.
"""
if self._all_teams is None:
self._all_teams = [
Team.from_db_team(db_team)
for db_team in db.session.query(DbTeam).all()
]
if include_none:
return self._all_teams
else:
predicate: Callable[[Team], bool] = lambda team: team != NONE_TEAM
return find_multiple_strict(predicate, self._all_teams)
def none_team(self) -> Team: def none_team(self) -> Team:
return NONE_TEAM return NONE_TEAM
def all_drivers(self, *, include_none: bool) -> List[Driver]: #
""" # Driver queries
Returns a list of all drivers in the database. #
"""
if self._all_drivers is None:
self._all_drivers = [
Driver.from_db_driver(db_driver)
for db_driver in db.session.query(DbDriver).all()
]
if include_none:
return self._all_drivers
else:
predicate: Callable[[Driver], bool] = lambda driver: driver != NONE_DRIVER
return find_multiple_strict(predicate, self._all_drivers)
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)
def drivers_for_wdc_gained(self) -> List[Driver]:
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))
def none_driver(self) -> Driver: def none_driver(self) -> Driver:
return NONE_DRIVER return NONE_DRIVER
@ -350,3 +302,14 @@ class TemplateModel:
return drivers_by return drivers_by
raise Exception("drivers_by encountered illegal combination of arguments") raise Exception("drivers_by encountered illegal combination of arguments")
#
# Race queries
#
def race_by(self, *, race_name: str) -> Race:
for race in self.all_races():
if race.name == race_name:
return race
raise Exception(f"Couldn't find race {race_name}")

View File

View File

@ -1,7 +1,7 @@
from urllib.parse import quote from urllib.parse import quote
from formula10.database.model.db_driver import DbDriver from formula10.database.model.db_driver import DbDriver
from formula10.frontend.model.team import NONE_TEAM, Team from formula10.domain.model.team import NONE_TEAM, Team
class Driver(): class Driver():

View File

@ -1,7 +1,7 @@
from formula10.database.model.db_race_guess import DbRaceGuess from formula10.database.model.db_race_guess import DbRaceGuess
from formula10.frontend.model.driver import Driver from formula10.domain.model.driver import Driver
from formula10.frontend.model.race import Race from formula10.domain.model.race import Race
from formula10.frontend.model.user import User from formula10.domain.model.user import User
class RaceGuess(): class RaceGuess():

View File

@ -3,8 +3,8 @@ from typing import Dict, List
from formula10.database.common_queries import find_single_driver_strict from formula10.database.common_queries import find_single_driver_strict
from formula10.database.model.db_race_result import DbRaceResult from formula10.database.model.db_race_result import DbRaceResult
from formula10.frontend.model.driver import NONE_DRIVER, Driver from formula10.domain.model.driver import NONE_DRIVER, Driver
from formula10.frontend.model.race import Race from formula10.domain.model.race import Race
class RaceResult: class RaceResult:
@ -70,8 +70,8 @@ class RaceResult:
return NotImplemented return NotImplemented
race: Race race: Race
standing: Dict[str, Driver] standing: Dict[str, Driver] # Always contains all 20 drivers, even if DNF'ed or excluded
initial_dnf: List[Driver] initial_dnf: List[Driver] # initial_dnf is empty if no-one DNF'ed
all_dnfs: List[Driver] all_dnfs: List[Driver]
standing_exclusions: List[Driver] standing_exclusions: List[Driver]
@ -86,6 +86,16 @@ class RaceResult:
return self.standing[position] return self.standing[position]
def driver_standing_position(self, driver: Driver) -> int | None:
if driver == NONE_DRIVER:
return None
for position, _driver in self.standing.items():
if driver == _driver and driver not in self.standing_exclusions:
return int(position)
return None
def driver_standing_position_string(self, driver: Driver) -> str: def driver_standing_position_string(self, driver: Driver) -> str:
if driver == NONE_DRIVER: if driver == NONE_DRIVER:
return "" return ""

View File

@ -2,9 +2,9 @@ import json
from typing import List from typing import List
from formula10.database.common_queries import find_single_driver_strict from formula10.database.common_queries import find_single_driver_strict
from formula10.database.model.db_season_guess import DbSeasonGuess from formula10.database.model.db_season_guess import DbSeasonGuess
from formula10.frontend.model.driver import Driver from formula10.domain.model.driver import Driver
from formula10.frontend.model.team import Team from formula10.domain.model.team import Team
from formula10.frontend.model.user import User from formula10.domain.model.user import User
class SeasonGuess(): class SeasonGuess():

View File

@ -0,0 +1,182 @@
from typing import Dict, List, overload
import numpy as np
from formula10.domain.domain_model import Model
from formula10.domain.model.driver import NONE_DRIVER
from formula10.domain.model.race_guess import RaceGuess
from formula10.domain.model.race_result import RaceResult
RACE_GUESS_OFFSET_POINTS: Dict[int, int] = {
3: 1,
2: 3,
1: 6,
0: 10
}
RACE_GUESS_DNF_POINTS: int = 10
SEASON_GUESS_HOT_TAKE_POINTS: int = 10
SEASON_GUESS_P2_POINTS: int = 10
SEASON_GUESS_OVERTAKES_POINTS: int = 10
SEASON_GUESS_DNF_POINTS: int = 10
SEASON_GUESS_GAINED_POINTS: int = 10
SEASON_GUESS_LOST_POINTS: int = 10
SEASON_GUESS_TEAMWINNER_CORRECT_POINTS: int = 3
SEASON_GUESS_TEAMWINNER_FALSE_POINTS: int = -3
SEASON_GUESS_PODIUMS_CORRECT_POINTS: int = 3
SEASON_GUESS_PODIUMS_FALSE_POINTS: int = -2
STANDING_2023: Dict[str, int] = {
"Max Verstappen": 1,
"Sergio Perez": 2,
"Lewis Hamilton": 3,
"Fernando Alonso": 4,
"Charles Leclerc": 5,
"Lando Norris": 6,
"Carlos Sainz": 7,
"George Russel": 8,
"Oscar Piastri": 9,
"Lance Stroll": 10,
"Pierre Gasly": 11,
"Esteban Ocon": 12,
"Alexander Albon": 13,
"Yuki Tsunoda": 14,
"Valtteri Bottas": 15,
"Nico Hulkenberg": 16,
"Daniel Ricciardo": 17,
"Zhou Guanyu": 18,
"Kevin Magnussen": 19,
"Logan Sargeant": 21
}
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)
if guessed_driver_position is None:
return 0
position_offset: int = abs(guessed_driver_position - race_guess.race.place_to_guess)
if position_offset not in RACE_GUESS_OFFSET_POINTS:
return 0
return RACE_GUESS_OFFSET_POINTS[position_offset]
def dnf_points(race_guess: RaceGuess, race_result: RaceResult) -> int:
if race_guess.dnf_guess in race_result.initial_dnf:
return RACE_GUESS_DNF_POINTS
if race_guess.dnf_guess == NONE_DRIVER and len(race_result.initial_dnf) == 0:
return RACE_GUESS_DNF_POINTS
return 0
class PointsModel(Model):
"""
This class bundles all data + functionality required to do points calculations.
"""
_points_per_step: Dict[str, List[int]] | None = None
def __init__(self):
Model.__init__(self)
def points_per_step(self) -> Dict[str, List[int]]:
"""
Returns a dictionary of lists, containing points per race for each user.
"""
if self._points_per_step is None:
self._points_per_step = dict()
for user in self.all_users():
self._points_per_step[user.name] = [0] * 24 # Start at index 1, like the race numbers
for race_guess in self.all_race_guesses():
user_name: str = race_guess.user.name
race_number: int = race_guess.race.number
race_result: RaceResult | None = self.race_result_by(race_name=race_guess.race.name)
if race_result is None:
continue
self._points_per_step[user_name][race_number] = standing_points(race_guess, race_result) + dnf_points(race_guess, race_result)
return self._points_per_step
def points_per_step_cumulative(self) -> Dict[str, List[int]]:
"""
Returns a dictionary of lists, containing cumulative points per race for each user.
"""
points_per_step_cumulative: Dict[str, List[int]] = dict()
for user_name, points in self.points_per_step().items():
points_per_step_cumulative[user_name] = np.cumsum(points).tolist()
return points_per_step_cumulative
@overload
def points_by(self, *, user_name: str) -> List[int]:
"""
Returns a list of points per race for a specific user.
"""
return self.points_by(user_name=user_name)
@overload
def points_by(self, *, race_name: str) -> Dict[str, int]:
"""
Returns a dictionary of points per user for a specific race.
"""
return self.points_by(race_name=race_name)
@overload
def points_by(self, *, user_name: str, race_name: str) -> int:
"""
Returns the points for a specific race for a specific user.
"""
return self.points_by(user_name=user_name, race_name=race_name)
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:
return self.points_per_step()[user_name]
if user_name is None and race_name is not None:
race_number: int = self.race_by(race_name=race_name).number
points_by_race: Dict[str, int] = dict()
for _user_name, points in self.points_per_step().items():
points_by_race[_user_name] = points[race_number]
return points_by_race
if user_name is not None and race_name is not None:
race_number: int = self.race_by(race_name=race_name).number
return self.points_per_step()[user_name][race_number]
raise Exception("points_by received an illegal combination of arguments")
def total_points_by(self, user_name: str) -> int:
"""
Returns the total number of points for a specific user.
"""
return sum(self.points_by(user_name=user_name))
def picks_count(self, user_name: str) -> int:
# Treat standing + dnf picks separately
return len(self.race_guesses_by(user_name=user_name)) * 2
def picks_with_points_count(self, user_name: str) -> int:
count: int = 0
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)
if race_result is None:
continue
if standing_points(race_guess, race_result) > 0:
count = count + 1
if dnf_points(race_guess, race_result) > 0:
count = count + 1
return count
def points_per_pick(self, user_name: str) -> float:
if self.picks_count(user_name) == 0:
return 0.0
return self.total_points_by(user_name) / self.picks_count(user_name)

View File

@ -0,0 +1,91 @@
from typing import List, Callable
from formula10 import ENABLE_TIMING
from formula10.domain.domain_model import Model
from formula10.domain.model.driver import Driver
from formula10.domain.model.race import Race
from formula10.domain.model.race_result import RaceResult
from formula10.domain.model.user import User
from formula10.database.validation import find_first_else_none, find_multiple_strict, race_has_started
class TemplateModel(Model):
"""
This class bundles all data + functionality required from inside a template.
"""
active_user: User | None = None
active_result: RaceResult | None = None
# RIC is excluded, since he didn't drive as many races 2023 as the others
_wdc_gained_excluded_abbrs: List[str] = ["RIC"]
def __init__(self, *, active_user_name: str | None, active_result_race_name: str | None):
Model.__init__(self)
if active_user_name is not None:
self.active_user = self.user_by(user_name=active_user_name, ignore=["Everyone"])
if active_result_race_name is not None:
self.active_result = self.race_result_by(race_name=active_result_race_name)
def race_guess_open(self, race: Race) -> bool:
return not race_has_started(race=race) if ENABLE_TIMING else True
def season_guess_open(self) -> bool:
return not race_has_started(race_name="Bahrain") if ENABLE_TIMING else True
def race_result_open(self, race_name: str) -> bool:
return race_has_started(race_name=race_name) if ENABLE_TIMING else True
def active_user_name_or_everyone(self) -> str:
return self.active_user.name if self.active_user is not None else "Everyone"
def active_user_name_sanitized_or_everyone(self) -> str:
return self.active_user.name_sanitized if self.active_user is not None else "Everyone"
def all_users_or_active_user(self) -> List[User]:
if self.active_user is not None:
return [self.active_user]
return self.all_users()
def first_race_without_result(self) -> Race | None:
"""
Returns the first race-object with no associated race result.
"""
results: List[RaceResult] = self.all_race_results()
if len(results) == 0:
return self.all_races()[-1] # all_races is sorted descending by number
most_recent_result: RaceResult = results[0]
predicate: Callable[[Race], bool] = lambda race: race.number == most_recent_result.race.number + 1
return find_first_else_none(predicate, self.all_races())
@property
def current_race(self) -> Race | None:
return self.first_race_without_result()
def active_result_race_name_or_current_race_name(self) -> str:
if self.active_result is not None:
return self.active_result.race.name
elif self.current_race is not None:
return self.current_race.name
else:
return self.all_races()[0].name
def active_result_race_name_or_current_race_name_sanitized(self) -> str:
if self.active_result is not None:
return self.active_result.race.name_sanitized
elif self.current_race is not None:
return self.current_race.name_sanitized
else:
return self.all_races()[0].name_sanitized
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)
def drivers_for_wdc_gained(self) -> List[Driver]:
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))

View File

@ -24,9 +24,9 @@
{% endmacro %} {% endmacro %}
{# Simple driver select for forms #} {# Simple driver select for forms #}
{% macro driver_select(name, label, include_none, drivers=none) %} {% macro driver_select(name, label, include_none, drivers=none, disabled=false) %}
<div class="form-floating"> <div class="form-floating">
<select name="{{ name }}" id="{{ name }}" class="form-select" aria-label="{{ name }}"> <select name="{{ name }}" id="{{ name }}" class="form-select" 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 %}
@ -42,9 +42,9 @@
{% 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) %} {% macro driver_select_with_preselect(driver_match, name, label, include_none, drivers=none, disabled=false) %}
<div class="form-floating"> <div class="form-floating">
<select name="{{ name }}" id="{{ name }}" class="form-select" aria-label="{{ name }}"> <select name="{{ name }}" id="{{ name }}" class="form-select" 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) %}
@ -75,9 +75,9 @@
{% endmacro %} {% endmacro %}
{# Simple team select for forms #} {# Simple team select for forms #}
{% macro team_select(name, label, include_none, teams=none) %} {% macro team_select(name, label, include_none, teams=none, disabled=false) %}
<div class="form-floating"> <div class="form-floating">
<select name="{{ name }}" id="{{ name }}" class="form-select" aria-label="{{ name }}"> <select name="{{ name }}" id="{{ name }}" class="form-select" 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 %}
@ -93,9 +93,9 @@
{% endmacro %} {% endmacro %}
{# 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) %} {% macro team_select_with_preselect(team_match, name, label, include_none, teams=none, disabled=false) %}
<div class="form-floating"> <div class="form-floating">
<select name="{{ name }}" id="{{ name }}" class="form-select" aria-label="{{ name }}"> <select name="{{ name }}" id="{{ name }}" class="form-select" 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) %}

View File

@ -51,17 +51,26 @@
{{ model.active_result_race_name_or_current_race_name() }} {{ model.active_result_race_name_or_current_race_name() }}
</h5> </h5>
<form action="/result-enter/{{ model.active_result_race_name_or_current_race_name_sanitized() }}" method="post"> {% set race_result_open=model.race_result_open(model.active_result_race_name_or_current_race_name()) %}
{% if race_result_open == true %}
{% set action_save_href = "/result-enter/" ~ model.active_result_race_name_or_current_race_name_sanitized() %}
{% else %}
{% set action_save_href = "" %}
{% endif %}
<form action="{{ action_save_href }}" method="post">
<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_drivers_or_active_result_standing_drivers() %} {% for driver in model.all_drivers_or_active_result_standing_drivers() %}
<li class="list-group-item p-1"><span id="place_number" class="fw-bold">P{{ "%02d" % loop.index }}</span>: </li> <li class="list-group-item p-1"><span id="place_number"
class="fw-bold">P{{ "%02d" % loop.index }}</span>:
</li>
{% endfor %} {% endfor %}
</ul> </ul>
<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_drivers_or_active_result_standing_drivers() %} {% for driver in model.all_drivers_or_active_result_standing_drivers() %}
<li class="list-group-item column p-1" draggable="true"> <li class="list-group-item {% if race_result_open == true %}column{% endif %} p-1" {% 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;">
@ -69,7 +78,8 @@
<div class="form-check form-check-reverse d-inline-block"> <div class="form-check form-check-reverse d-inline-block">
<input type="checkbox" class="form-check-input" value="{{ driver.name }}" <input type="checkbox" class="form-check-input" value="{{ driver.name }}"
id="first-dnf-{{ driver.name }}" name="first-dnf-drivers" id="first-dnf-{{ driver.name }}" name="first-dnf-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.initial_dnf) %}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 %}>
<label for="first-dnf-{{ driver.name }}" <label for="first-dnf-{{ driver.name }}"
class="form-check-label text-muted">1. DNF</label> class="form-check-label text-muted">1. DNF</label>
</div> </div>
@ -78,7 +88,8 @@
<div class="form-check form-check-reverse d-inline-block mx-2"> <div class="form-check form-check-reverse d-inline-block mx-2">
<input type="checkbox" class="form-check-input" value="{{ driver.name }}" <input type="checkbox" class="form-check-input" value="{{ driver.name }}"
id="dnf-{{ driver.name }}" name="dnf-drivers" id="dnf-{{ driver.name }}" name="dnf-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.all_dnfs) %}checked{% endif %}> {% 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.name }}" <label for="dnf-{{ driver.name }}"
class="form-check-label text-muted">DNF</label> class="form-check-label text-muted">DNF</label>
</div> </div>
@ -87,7 +98,8 @@
<div class="form-check form-check-reverse d-inline-block"> <div class="form-check form-check-reverse d-inline-block">
<input type="checkbox" class="form-check-input" value="{{ driver.name }}" <input type="checkbox" class="form-check-input" value="{{ driver.name }}"
id="exclude-{{ driver.name }}" name="excluded-drivers" id="exclude-{{ driver.name }}" name="excluded-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.standing_exclusions) %}checked{% endif %}> {% 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.name }}" <label for="exclude-{{ driver.name }}"
class="form-check-label text-muted" data-bs-toggle="tooltip" class="form-check-label text-muted" data-bs-toggle="tooltip"
title="Driver is not counted for standing">NC</label> title="Driver is not counted for standing">NC</label>
@ -100,7 +112,8 @@
{% endfor %} {% endfor %}
</ul> </ul>
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save"> <input type="submit" class="btn btn-danger mt-2 w-100" value="Save"
{% if race_result_open == false %}disabled="disabled"{% endif %}>
</form> </form>
</div> </div>

View File

@ -17,13 +17,13 @@
<table class="table table-bordered table-sm table-responsive shadow-sm"> <table class="table table-bordered table-sm table-responsive shadow-sm">
<thead> <thead>
<tr> <tr>
<th scope="col" rowspan="2" class="text-center" style="width: 125px;">Race</th> <th scope="col" rowspan="2" class="text-center" style="width: 125px !important;">Race</th>
<th scope="col" {% if model.active_user is none %}colspan="{{ model.all_users() | length }}"{% endif %} <th scope="col" {% if model.active_user is none %}colspan="{{ model.all_users() | length }}"{% endif %}
class="text-center">Call class="text-center">Call
</th> </th>
<th scope="col" rowspan="2" class="text-center" style="width: 125px;">Result</th> <th scope="col" rowspan="2" class="text-center" style="width: 125px !important;">Result</th>
</tr> </tr>
</thead> </thead>
@ -35,7 +35,7 @@
{# 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> <td class="text-center text-nowrap" style="width: 1%;">{{ model.active_user.name }}</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;">
@ -101,7 +101,8 @@
</td> </td>
<td> <td>
{% if model.race_guess_open(model.current_race) == true %} {% set race_guess_open = model.race_guess_open(model.current_race) %}
{% if race_guess_open == true %}
{% set action_save_href = "/race-guess/" ~ model.current_race.name_sanitized ~ "/" ~ model.active_user.name_sanitized %} {% set action_save_href = "/race-guess/" ~ model.current_race.name_sanitized ~ "/" ~ model.active_user.name_sanitized %}
{% set action_delete_href = "/race-guess-delete/" ~ model.current_race.name_sanitized ~ "/" ~ model.active_user.name_sanitized %} {% set action_delete_href = "/race-guess-delete/" ~ model.current_race.name_sanitized ~ "/" ~ model.active_user.name_sanitized %}
{% else %} {% else %}
@ -114,21 +115,21 @@
{% set user_guess = model.race_guesses_by(user_name=model.active_user.name, race_name=model.current_race.name) %} {% set user_guess = model.race_guesses_by(user_name=model.active_user.name, race_name=model.current_race.name) %}
{# Driver PXX Select #} {# Driver PXX Select #}
{{ driver_select_with_preselect(driver_match=user_guess.pxx_guess, name="pxxselect", label="P" ~ model.current_race.place_to_guess ~ ":", include_none=true) }} {{ driver_select_with_preselect(driver_match=user_guess.pxx_guess, name="pxxselect", label="P" ~ model.current_race.place_to_guess ~ ":", include_none=true, disabled=not race_guess_open) }}
<div class="mt-2"></div> <div class="mt-2"></div>
{# Driver DNF Select #} {# Driver DNF Select #}
{{ driver_select_with_preselect(driver_match=user_guess.dnf_guess, name="dnfselect", label="DNF:", include_none=true) }} {{ driver_select_with_preselect(driver_match=user_guess.dnf_guess, name="dnfselect", label="DNF:", include_none=true, disabled=not race_guess_open) }}
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save" <input type="submit" class="btn btn-danger mt-2 w-100" value="Save"
{% if model.race_guess_open(model.current_race) == false %}disabled="disabled"{% endif %}> {% if race_guess_open == false %}disabled="disabled"{% endif %}>
</form> </form>
{# Delete guess #} {# Delete guess #}
<form action="{{ action_delete_href }}" method="post"> <form action="{{ action_delete_href }}" method="post">
<input type="submit" class="btn btn-dark mt-2 w-100" value="Delete" <input type="submit" class="btn btn-dark mt-2 w-100" value="Delete"
{% if model.race_guess_open(model.current_race) == false %}disabled{% endif %}> {% if race_guess_open == false %}disabled{% endif %}>
</form> </form>
</td> </td>

View File

@ -28,7 +28,8 @@
{% set user_guess = model.season_guesses_by(user_name=user.name) %} {% set user_guess = model.season_guesses_by(user_name=user.name) %}
{% if model.season_guess_open() == true %} {% set season_guess_open = model.season_guess_open() %}
{% if season_guess_open == true %}
{% set action_save_href = "/season-guess/" ~ user.name %} {% set action_save_href = "/season-guess/" ~ user.name %}
{% else %} {% else %}
{% set action_save_href = "" %} {% set action_save_href = "" %}
@ -37,33 +38,31 @@
{# Hot Take #} {# Hot Take #}
<div class="form-floating"> <div class="form-floating">
{% if user_guess is not none %}
<textarea class="form-control" id="hot-take-input-{{ user.name }}" name="hottakeselect" <textarea class="form-control" id="hot-take-input-{{ user.name }}" name="hottakeselect"
style="height: 150px">{{ user_guess.hot_take_string() }}</textarea> style="height: 150px"
{% else %} {% if season_guess_open == false %}disabled="disabled"{% endif %}>
<textarea class="form-control" id="hot-take-input-{{ user.name }}" name="hottakeselect" {%- if user_guess is not none -%}{{ user_guess.hot_take_string() }}{%- endif -%}
style="height: 150px"></textarea> </textarea>
{% endif %}
<label for="hot-take-input-{{ user.name }}" class="text-primary">Hot Take:</label> <label for="hot-take-input-{{ user.name }}" class="text-primary">Hot Take:</label>
</div> </div>
{# P2 Constructor #} {# P2 Constructor #}
<div class="mt-2"> <div class="mt-2">
{{ team_select_with_preselect(team_match=user_guess.p2_wcc, name="p2select", label="P2 in WCC:", include_none=false) }} {{ team_select_with_preselect(team_match=user_guess.p2_wcc, name="p2select", label="P2 in WCC:", include_none=false, disabled=not season_guess_open) }}
</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", label="Most overtakes:", include_none=false) }} {{ driver_select_with_preselect(driver_match=user_guess.most_overtakes, name="overtakeselect", label="Most overtakes:", include_none=false, disabled=not season_guess_open) }}
{{ driver_select_with_preselect(driver_match=user_guess.most_dnfs, name="dnfselect", label="Most DNFs:", include_none=false) }} {{ driver_select_with_preselect(driver_match=user_guess.most_dnfs, name="dnfselect", label="Most DNFs:", include_none=false, disabled=not season_guess_open) }}
</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", label="Most WDC places gained:", include_none=false, drivers=model.drivers_for_wdc_gained()) }} {{ driver_select_with_preselect(driver_match=user_guess.most_wdc_gained, name="gainedselect", label="Most WDC places gained:", include_none=false, drivers=model.drivers_for_wdc_gained(), disabled=not season_guess_open) }}
{{ driver_select_with_preselect(driver_match=user_guess.most_wdc_lost, name="lostselect", label="Most WDC places lost:", include_none=false) }} {{ driver_select_with_preselect(driver_match=user_guess.most_wdc_lost, name="lostselect", label="Most WDC places lost:", include_none=false, disabled=not season_guess_open) }}
</div> </div>
{# Team-internal Winners #} {# Team-internal Winners #}
@ -81,7 +80,8 @@
name="teamwinner-{{ team.name }}" name="teamwinner-{{ team.name }}"
id="teamwinner-{{ team.name }}-1-{{ user.name }}" id="teamwinner-{{ team.name }}-1-{{ user.name }}"
value="{{ driver_a.name }}" value="{{ driver_a.name }}"
{% 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 %}>
<label class="form-check-label" <label class="form-check-label"
for="teamwinner-{{ team.name }}-1-{{ user.name }}">{{ driver_a.name }}</label> for="teamwinner-{{ team.name }}-1-{{ user.name }}">{{ driver_a.name }}</label>
</div> </div>
@ -93,7 +93,8 @@
name="teamwinner-{{ team.name }}" name="teamwinner-{{ team.name }}"
id="teamwinner-{{ team.name }}-2-{{ user.name }}" id="teamwinner-{{ team.name }}-2-{{ user.name }}"
value="{{ driver_b.name }}" value="{{ driver_b.name }}"
{% 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 %}>
<label class="form-check-label" <label class="form-check-label"
for="teamwinner-{{ team.name }}-2-{{ user.name }}">{{ driver_b.name }}</label> for="teamwinner-{{ team.name }}-2-{{ user.name }}">{{ driver_b.name }}</label>
</div> </div>
@ -115,7 +116,8 @@
name="podiumdrivers" name="podiumdrivers"
id="podium-{{ driver_a.name }}-{{ user.name }}" id="podium-{{ driver_a.name }}-{{ user.name }}"
value="{{ driver_a.name }}" value="{{ driver_a.name }}"
{% 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 %}>
<label class="form-check-label" <label class="form-check-label"
for="podium-{{ driver_a.name }}-{{ user.name }}">{{ driver_a.name }}</label> for="podium-{{ driver_a.name }}-{{ user.name }}">{{ driver_a.name }}</label>
</div> </div>
@ -127,7 +129,8 @@
name="podiumdrivers" name="podiumdrivers"
id="podium-{{ driver_b.name }}-{{ user.name }}" id="podium-{{ driver_b.name }}-{{ user.name }}"
value="{{ driver_b.name }}" value="{{ driver_b.name }}"
{% 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 %}>
<label class="form-check-label" <label class="form-check-label"
for="podium-{{ driver_b.name }}-{{ user.name }}">{{ driver_b.name }}</label> for="podium-{{ driver_b.name }}-{{ user.name }}">{{ driver_b.name }}</label>
</div> </div>
@ -135,7 +138,8 @@
{% endfor %} {% endfor %}
</div> </div>
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save" {% if model.season_guess_open() == false %}disabled{% endif %}> <input type="submit" class="btn btn-danger mt-2 w-100" value="Save"
{% if season_guess_open == false %}disabled{% endif %}>
</form> </form>
</div> </div>

View File

@ -0,0 +1,56 @@
{% extends 'base.jinja' %}
{% block title %}Formula 10 - Leaderboard{% endblock title %}
{% set active_page = "/graphs" %}
{% block body %}
<div class="card">
<div class="card-body">
<h5 class="card-title">Leaderboard</h5>
<h6 class="card-subtitle">Points only include race picks</h6>
<table class="table table-bordered table-sm table-responsive mt-3">
<thead>
<tr>
<th scope="col" class="text-center" style="width: 1%;">User</th>
<th scope="col" class="text-center" style="width: 1%;">Points</th>
<th scope="col" class="text-center" style="width: 1%;">Total picks</th>
<th scope="col" class="text-center" style="width: 1%;">Correct picks</th>
<th scope="col" class="text-center" style="width: 1%;">Points per pick</th>
</tr>
</thead>
<tbody>
{% for user in model.all_users() %}
<tr>
<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.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">{{ "%0.2f" % points.points_per_pick(user.name) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{# <div class="card mt-2">#}
{# <div class="card-body">#}
{# <h5 class="card-title">History</h5>#}
{# Line chart of point history with a line per user #}
{# </div>#}
{# </div>#}
{# <div class="card mt-2">#}
{# <div class="card-body">#}
{# <h5 class="card-title">Statistics</h5>#}
{# Various statistics: Driver voted most for DNF #}
{# </div>#}
{# </div>#}
{% endblock body %}

View File

@ -4,3 +4,5 @@ numpy
flask flask
flask-sqlalchemy flask-sqlalchemy
sqlalchemy sqlalchemy
pytest