Split frontend model from backend model
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 26s

This commit is contained in:
2024-02-25 15:09:59 +01:00
parent a9b1bc4403
commit 991a1a177e
38 changed files with 1094 additions and 930 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ __pycache__
instance instance
data/dynamic_export data/dynamic_export
data/dynamic_export

View File

@ -1,8 +0,0 @@
name
Christoph
Angela Merkel
Xi Jinping
Donald Trump
Joe Biden
Henri
Vinzent
1 name
2 Christoph
3 Angela Merkel
4 Xi Jinping
5 Donald Trump
6 Joe Biden
7 Henri
8 Vinzent

View File

@ -1,4 +1,5 @@
name name
None
Alpine Alpine
Aston Martin Aston Martin
Ferrari Ferrari

1 name
2 None
3 Alpine
4 Aston Martin
5 Ferrari

View File

@ -18,17 +18,12 @@ import formula10.controller.admin_controller # type: ignore
# TODO # TODO
# General # General
# Split the database model from the frontend-model/template-model/domain-model
# - Move most of the template logic into this
# - Allow exclusion of e.g. most-gained driver and other stuff
# - Choose "place to guess" late before the race? Make a page for this # - Choose "place to guess" late before the race? Make a page for this
# - Rules page # - Rules page
# - Make user order changeable using drag'n'drop? # - Make user order changeable using drag'n'drop?
# - Show place when entering race result (would require updating the drag'n'drop code...) # - Show place when entering race result (would require updating the drag'n'drop code...)
# - Show cards of previous race results, like with season guesses? # - Show cards of previous race results, like with season guesses?
# - Make the season card grid left-aligned? So e.g. 2 cards are not spread over the whole screen with large gaps?
# Statistics # Statistics
# - Auto calculate points # - Auto calculate points

View File

@ -3,8 +3,8 @@ from urllib.parse import unquote
from flask import redirect, render_template, request from flask import redirect, render_template, request
from werkzeug import Response from werkzeug import Response
from formula10.database.update_query_util import update_race_result, update_user from formula10.database.update_queries import update_race_result, update_user
from formula10.database.import_export_util import export_dynamic_data, reload_dynamic_data, reload_static_data from formula10.database.import_export import export_dynamic_data, reload_dynamic_data, reload_static_data
from formula10.frontend.template_model import TemplateModel from formula10.frontend.template_model import TemplateModel
from formula10 import app from formula10 import app
@ -42,10 +42,10 @@ def result_root() -> Response:
@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() model = TemplateModel(active_user_name=None,
return render_template("enter.jinja", active_result_race_name=race_name)
active_result=model.race_result_by(race_name=race_name),
model=model) return render_template("enter.jinja", model=model)
@app.route("/result-enter/<race_name>", methods=["POST"]) @app.route("/result-enter/<race_name>", methods=["POST"])
@ -61,9 +61,10 @@ def result_enter_post(race_name: str) -> Response:
@app.route("/user") @app.route("/user")
def user_root() -> str: def user_root() -> str:
model = TemplateModel() model = TemplateModel(active_user_name=None,
return render_template("users.jinja", active_result_race_name=None)
model=model)
return render_template("users.jinja", model=model)
@app.route("/user-add", methods=["POST"]) @app.route("/user-add", methods=["POST"])

View File

@ -2,7 +2,7 @@ from urllib.parse import unquote
from flask import redirect, render_template, request from flask import redirect, render_template, request
from werkzeug import Response from werkzeug import Response
from formula10.database.update_query_util 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.frontend.template_model import TemplateModel
from formula10 import app from formula10 import app
@ -20,10 +20,10 @@ def race_root() -> Response:
@app.route("/race/<user_name>") @app.route("/race/<user_name>")
def race_active_user(user_name: str) -> str: def race_active_user(user_name: str) -> str:
user_name = unquote(user_name) user_name = unquote(user_name)
model = TemplateModel() model = TemplateModel(active_user_name=user_name,
return render_template("race.jinja", active_result_race_name=None)
active_user=model.user_by(user_name=user_name, ignore=["Everyone"]),
model=model) return render_template("race.jinja", model=model)
@app.route("/race-guess/<race_name>/<user_name>", methods=["POST"]) @app.route("/race-guess/<race_name>/<user_name>", methods=["POST"])

View File

@ -3,8 +3,8 @@ from urllib.parse import unquote
from flask import redirect, render_template, request from flask import redirect, render_template, request
from werkzeug import Response from werkzeug import Response
from formula10.database.model.team import Team from formula10.database.model.db_team import DbTeam
from formula10.database.update_query_util import update_season_guess from formula10.database.update_queries import update_season_guess
from formula10.frontend.template_model import TemplateModel from formula10.frontend.template_model import TemplateModel
from formula10 import app, db from formula10 import app, db
@ -17,10 +17,10 @@ def season_root() -> Response:
@app.route("/season/<user_name>") @app.route("/season/<user_name>")
def season_active_user(user_name: str) -> str: def season_active_user(user_name: str) -> str:
user_name = unquote(user_name) user_name = unquote(user_name)
model = TemplateModel() model = TemplateModel(active_user_name=user_name,
return render_template("season.jinja", active_result_race_name=None)
active_user=model.user_by(user_name=user_name, ignore=["Everyone"]),
model=model) return render_template("season.jinja", model=model)
@app.route("/season-guess/<user_name>", methods=["POST"]) @app.route("/season-guess/<user_name>", methods=["POST"])
@ -36,7 +36,7 @@ def season_guess_post(user_name: str) -> Response:
] ]
# TODO: This is pretty ugly, to do queries in the controller # TODO: This is pretty ugly, to do queries in the controller
team_winner_guesses: List[str | None] = [ team_winner_guesses: List[str | None] = [
request.form.get(f"teamwinner-{team.name}") for team in db.session.query(Team).all() request.form.get(f"teamwinner-{team.name}") for team in db.session.query(DbTeam).all()
] ]
podium_driver_guesses: List[str] = request.form.getlist("podiumdrivers") podium_driver_guesses: List[str] = request.form.getlist("podiumdrivers")

View File

@ -0,0 +1,23 @@
from formula10.database.model.db_driver import DbDriver
from formula10.database.model.db_race_result import DbRaceResult
from formula10.database.model.db_user import DbUser
from formula10 import db
def race_has_result(race_name: str) -> bool:
return db.session.query(DbRaceResult).filter_by(race_name=race_name).first() is not None
def user_exists_and_enabled(user_name: str) -> bool:
return db.session.query(DbUser).filter_by(name=user_name, enabled=True).first() is not None
def user_exists_and_disabled(user_name: str) -> bool:
return db.session.query(DbUser).filter_by(name=user_name, enabled=False).first() is not None
def find_single_driver_strict(driver_name: str) -> DbDriver:
db_driver: DbDriver | None = db.session.query(DbDriver).filter_by(name=driver_name).first()
if db_driver is None:
raise Exception(f"Could not find driver with name {driver_name} in database")
return db_driver

View File

@ -1,10 +0,0 @@
from formula10.database.model.race_result import RaceResult
from formula10.database.model.user import User
from formula10 import db
def race_has_result(race_name: str) -> bool:
return db.session.query(RaceResult).filter_by(race_name=race_name).first() is not None
def user_exists(user_name: str) -> bool:
return db.session.query(User).filter_by(name=user_name).first() is not None

View File

@ -2,16 +2,14 @@ import csv
import os.path import os.path
from typing import List, Any from typing import List, Any
from formula10.database.model.driver import Driver
from formula10.database.model.podium_drivers import PodiumDrivers
from formula10.database.model.race import Race
from formula10.database.model.race_guess import RaceGuess
from formula10.database.model.race_result import RaceResult
from formula10.database.model.season_guess import SeasonGuess
from formula10.database.model.team import Team
from formula10.database.model.team_winners import TeamWinners
from formula10.database.model.user import User
from formula10 import db from formula10 import db
from formula10.database.model.db_driver import DbDriver
from formula10.database.model.db_race import DbRace
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_team import DbTeam
from formula10.database.model.db_user import DbUser
def load_csv(filename: str) -> List[List[str]]: def load_csv(filename: str) -> List[List[str]]:
@ -44,17 +42,17 @@ def reload_static_data():
db.create_all() db.create_all()
# Clear static data # Clear static data
db.session.query(Team).delete() db.session.query(DbTeam).delete()
db.session.query(Driver).delete() db.session.query(DbDriver).delete()
db.session.query(Race).delete() db.session.query(DbRace).delete()
# Reload static data # Reload static data
for row in load_csv("data/static_import/teams.csv"): for row in load_csv("data/static_import/teams.csv"):
db.session.add(Team.from_csv(row)) db.session.add(DbTeam.from_csv(row))
for row in load_csv("data/static_import/drivers.csv"): for row in load_csv("data/static_import/drivers.csv"):
db.session.add(Driver.from_csv(row)) db.session.add(DbDriver.from_csv(row))
for row in load_csv("data/static_import/races.csv"): for row in load_csv("data/static_import/races.csv"):
db.session.add(Race.from_csv(row)) db.session.add(DbRace.from_csv(row))
db.session.commit() db.session.commit()
@ -65,26 +63,20 @@ def reload_dynamic_data():
db.create_all() db.create_all()
# Clear dynamic data # Clear dynamic data
db.session.query(User).delete() db.session.query(DbUser).delete()
db.session.query(RaceResult).delete() db.session.query(DbRaceResult).delete()
db.session.query(RaceGuess).delete() db.session.query(DbRaceGuess).delete()
db.session.query(TeamWinners).delete() db.session.query(DbSeasonGuess).delete()
db.session.query(PodiumDrivers).delete()
db.session.query(SeasonGuess).delete()
# Reload dynamic data # Reload dynamic data
for row in load_csv("data/dynamic_export/users.csv"): for row in load_csv("data/dynamic_export/users.csv"):
db.session.add(User.from_csv(row)) db.session.add(DbUser.from_csv(row))
for row in load_csv("data/dynamic_export/raceresults.csv"): for row in load_csv("data/dynamic_export/raceresults.csv"):
db.session.add(RaceResult.from_csv(row)) db.session.add(DbRaceResult.from_csv(row))
for row in load_csv("data/dynamic_export/raceguesses.csv"): for row in load_csv("data/dynamic_export/raceguesses.csv"):
db.session.add(RaceGuess.from_csv(row)) db.session.add(DbRaceGuess.from_csv(row))
for row in load_csv("data/dynamic_export/teamwinners.csv"):
db.session.add(TeamWinners.from_csv(row))
for row in load_csv("data/dynamic_export/podiumdrivers.csv"):
db.session.add(PodiumDrivers.from_csv(row))
for row in load_csv("data/dynamic_export/seasonguesses.csv"): for row in load_csv("data/dynamic_export/seasonguesses.csv"):
db.session.add(SeasonGuess.from_csv(row)) db.session.add(DbSeasonGuess.from_csv(row))
db.session.commit() db.session.commit()
@ -92,16 +84,12 @@ def reload_dynamic_data():
def export_dynamic_data(): def export_dynamic_data():
print("Exporting Userdata...") print("Exporting Userdata...")
users: List[User] = db.session.query(User).all() users: List[DbUser] = db.session.query(DbUser).all()
raceresults: List[RaceResult] = db.session.query(RaceResult).all() raceresults: List[DbRaceResult] = db.session.query(DbRaceResult).all()
raceguesses: List[RaceGuess] = db.session.query(RaceGuess).all() raceguesses: List[DbRaceGuess] = db.session.query(DbRaceGuess).all()
teamwinners: List[TeamWinners] = db.session.query(TeamWinners).all() seasonguesses: List[DbSeasonGuess] = db.session.query(DbSeasonGuess).all()
podiumdrivers: List[PodiumDrivers] = db.session.query(PodiumDrivers).all()
seasonguesses: List[SeasonGuess] = db.session.query(SeasonGuess).all()
write_csv("data/dynamic_export/users.csv", users) write_csv("data/dynamic_export/users.csv", users)
write_csv("data/dynamic_export/raceresults.csv", raceresults) write_csv("data/dynamic_export/raceresults.csv", raceresults)
write_csv("data/dynamic_export/raceguesses.csv", raceguesses) write_csv("data/dynamic_export/raceguesses.csv", raceguesses)
write_csv("data/dynamic_export/teamwinners.csv", teamwinners)
write_csv("data/dynamic_export/podiumdrivers.csv", podiumdrivers)
write_csv("data/dynamic_export/seasonguesses.csv", seasonguesses) write_csv("data/dynamic_export/seasonguesses.csv", seasonguesses)

View File

@ -2,25 +2,27 @@ from typing import List
from sqlalchemy import String, ForeignKey from sqlalchemy import String, ForeignKey
from sqlalchemy.orm import mapped_column, Mapped, relationship from sqlalchemy.orm import mapped_column, Mapped, relationship
from formula10.database.model.team import Team from formula10.database.model.db_team import DbTeam
from formula10 import db from formula10 import db
class Driver(db.Model): class DbDriver(db.Model):
""" """
A F1 driver. A F1 driver.
It stores the corresponding team + name abbreviation. It stores the corresponding team + name abbreviation.
""" """
__tablename__ = "driver" __tablename__ = "driver"
@staticmethod def __init__(self, *, name: str):
def from_csv(row: List[str]): self.name = name # Primary key
driver: Driver = Driver()
driver.name = str(row[0]) @classmethod
driver.abbr = str(row[1]) def from_csv(cls, row: List[str]):
driver.team_name = str(row[2]) db_driver: DbDriver = cls(name=str(row[0]))
driver.country_code = str(row[3]) db_driver.abbr = str(row[1])
return driver db_driver.team_name = str(row[2])
db_driver.country_code = str(row[3])
return db_driver
name: Mapped[str] = mapped_column(String(32), primary_key=True) name: Mapped[str] = mapped_column(String(32), primary_key=True)
abbr: Mapped[str] = mapped_column(String(4)) abbr: Mapped[str] = mapped_column(String(4))
@ -28,4 +30,4 @@ class Driver(db.Model):
country_code: Mapped[str] = mapped_column(String(2)) # alpha-2 code country_code: Mapped[str] = mapped_column(String(2)) # alpha-2 code
# Relationships # Relationships
team: Mapped["Team"] = relationship("Team", foreign_keys=[team_name]) team: Mapped[DbTeam] = relationship("DbTeam", foreign_keys=[team_name])

View File

@ -1,31 +1,32 @@
from datetime import datetime from datetime import datetime
from typing import List from typing import List
from urllib.parse import quote
from sqlalchemy import 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
class Race(db.Model): class DbRace(db.Model):
""" """
A single race at a certain date and GrandPrix in the calendar. A single race at a certain date and GrandPrix in the calendar.
It stores the place to guess for this race. It stores the place to guess for this race.
""" """
__tablename__ = "race" __tablename__ = "race"
@staticmethod def __init__(self, *, name: str, number: int, date: datetime, pxx: int):
def from_csv(row: List[str]): self.name = name # Primary key
race: Race = Race()
race.name = str(row[0])
race.number = int(row[1])
race.date = datetime.strptime(row[2], "%Y-%m-%d")
race.pxx = int(row[3])
return race
@property self.number = number
def name_sanitized(self) -> str: self.date = date
return quote(self.name) self.pxx = pxx
@classmethod
def from_csv(cls, row: List[str]):
db_race: DbRace = cls(name=str(row[0]),
number=int(row[1]),
date=datetime.strptime(row[2], "%Y-%m-%d"),
pxx=int(row[3]))
return db_race
name: Mapped[str] = mapped_column(String(64), primary_key=True) name: Mapped[str] = mapped_column(String(64), primary_key=True)
number: Mapped[int] = mapped_column(Integer) number: Mapped[int] = mapped_column(Integer)

View File

@ -0,0 +1,51 @@
from typing import Any, List
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from formula10.database.model.db_user import DbUser
from formula10.database.model.db_race import DbRace
from formula10.database.model.db_driver import DbDriver
from formula10 import db
class DbRaceGuess(db.Model):
"""
A guess a user made for a race.
It stores the corresponding race and the guessed drivers for PXX and DNF.
"""
__tablename__ = "raceguess"
__csv_header__ = ["user_name", "race_name", "pxx_driver_name", "dnf_driver_name"]
def __init__(self, *, user_name: str, race_name: str, pxx_driver_name: str, dnf_driver_name: str):
self.user_name = user_name # Primary key
self.race_name = race_name # Primary key
self.dnf_driver_name = dnf_driver_name
self.pxx_driver_name = pxx_driver_name
@classmethod
def from_csv(cls, row: List[str]):
db_race_guess: DbRaceGuess = cls(user_name=str(row[0]),
race_name=str(row[1]),
pxx_driver_name=str(row[2]),
dnf_driver_name=str(row[3]))
return db_race_guess
def to_csv(self) -> List[Any]:
return [
self.user_name,
self.race_name,
self.pxx_driver_name,
self.dnf_driver_name
]
user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True)
race_name: Mapped[str] = mapped_column(ForeignKey("race.name"), primary_key=True)
pxx_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name"))
dnf_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name"))
# Relationships
user: Mapped[DbUser] = relationship("DbUser", foreign_keys=[user_name])
race: Mapped[DbRace] = relationship("DbRace", foreign_keys=[race_name])
pxx: Mapped[DbDriver] = relationship("DbDriver", foreign_keys=[pxx_driver_name])
dnf: Mapped[DbDriver] = relationship("DbDriver", foreign_keys=[dnf_driver_name])

View File

@ -0,0 +1,49 @@
from typing import Any, List
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from formula10.database.model.db_race import DbRace
from formula10 import db
class DbRaceResult(db.Model):
"""
The result of a past race.
It stores the corresponding race and dictionaries of place-/dnf-order and a list of drivers that are excluded from the standings for this race.
"""
__tablename__ = "raceresult"
__csv_header__ = ["race_name", "pxx_driver_names_json", "first_dnf_driver_names_json", "dnf_driver_names_json", "excluded_driver_names_json"]
def __init__(self, *, race_name: str, pxx_driver_names_json: str, first_dnf_driver_names_json: str, dnf_driver_names_json: str, excluded_driver_names_json: str):
self.race_name = race_name # Primary key
self.pxx_driver_names_json = pxx_driver_names_json
self.first_dnf_driver_names_json = first_dnf_driver_names_json
self.dnf_driver_names_json = dnf_driver_names_json
self.excluded_driver_names_json = excluded_driver_names_json
@classmethod
def from_csv(cls, row: List[str]):
db_race_result: DbRaceResult = cls(race_name=str(row[0]),
pxx_driver_names_json=str(row[1]),
first_dnf_driver_names_json=str(row[2]),
dnf_driver_names_json=str(row[3]),
excluded_driver_names_json=str(row[4]))
return db_race_result
def to_csv(self) -> List[Any]:
return [
self.race_name,
self.pxx_driver_names_json,
self.first_dnf_driver_names_json,
self.dnf_driver_names_json,
self.excluded_driver_names_json
]
race_name: Mapped[str] = mapped_column(ForeignKey("race.name"), primary_key=True)
pxx_driver_names_json: Mapped[str] = mapped_column(String(1024))
first_dnf_driver_names_json: Mapped[str] = mapped_column(String(1024))
dnf_driver_names_json: Mapped[str] = mapped_column(String(1024))
excluded_driver_names_json: Mapped[str] = mapped_column(String(1024))
# Relationships
race: Mapped[DbRace] = relationship("DbRace", foreign_keys=[race_name])

View File

@ -0,0 +1,67 @@
from typing import Any, List
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from formula10.database.model.db_driver import DbDriver
from formula10.database.model.db_team import DbTeam
from formula10.database.model.db_user import DbUser
from formula10 import db
class DbSeasonGuess(db.Model):
"""
A collection of bonus guesses for the entire season.
"""
__tablename__ = "seasonguess"
__csv_header__ = ["user_name", "hot_take", "p2_team_name",
"overtake_driver_name", "dnf_driver_name", "gained_driver_name", "lost_driver_name",
"team_winners_driver_names_json", "podium_drivers_driver_names_json"]
def __init__(self, *, user_name: str, team_winners_driver_names_json: str, podium_drivers_driver_names_json: str):
self.user_name = user_name # Primary key
self.team_winners_driver_names_json = team_winners_driver_names_json
self.podium_drivers_driver_names_json = podium_drivers_driver_names_json
@classmethod
def from_csv(cls, row: List[str]):
db_season_guess: DbSeasonGuess = cls(user_name=str(row[0]),
team_winners_driver_names_json=str(row[7]),
podium_drivers_driver_names_json=str(row[8]))
db_season_guess.hot_take = str(row[1])
db_season_guess.p2_team_name = str(row[2])
db_season_guess.overtake_driver_name = str(row[3])
db_season_guess.dnf_driver_name = str(row[4])
db_season_guess.gained_driver_name = str(row[5])
db_season_guess.lost_driver_name = str(row[6])
return db_season_guess
def to_csv(self) -> List[Any]:
return [
self.user_name,
self.hot_take,
self.p2_team_name,
self.overtake_driver_name,
self.dnf_driver_name,
self.gained_driver_name,
self.lost_driver_name,
self.team_winners_driver_names_json,
self.podium_drivers_driver_names_json
]
user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True)
hot_take: Mapped[str | None] = mapped_column(String(512), nullable=True)
p2_team_name: Mapped[str | None] = mapped_column(ForeignKey("team.name"), nullable=True)
overtake_driver_name: Mapped[str | None] = mapped_column(ForeignKey("driver.name"), nullable=True)
dnf_driver_name: Mapped[str | None] = mapped_column(ForeignKey("driver.name"), nullable=True)
gained_driver_name: Mapped[str | None] = mapped_column(ForeignKey("driver.name"), nullable=True)
lost_driver_name: Mapped[str | None] = mapped_column(ForeignKey("driver.name"), nullable=True)
team_winners_driver_names_json: Mapped[str] = mapped_column(String(1024))
podium_drivers_driver_names_json: Mapped[str] = mapped_column(String(1024))
# Relationships
user: Mapped[DbUser] = relationship("DbUser", foreign_keys=[user_name])
p2_team: Mapped[DbTeam | None] = relationship("DbTeam", foreign_keys=[p2_team_name])
overtake_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[overtake_driver_name])
dnf_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[dnf_driver_name])
gained_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[gained_driver_name])
lost_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[lost_driver_name])

View File

@ -4,16 +4,19 @@ from sqlalchemy.orm import Mapped, mapped_column
from formula10 import db from formula10 import db
class Team(db.Model):
class DbTeam(db.Model):
""" """
A constructor/team (name only). A constructor/team (name only).
""" """
__tablename__ = "team" __tablename__ = "team"
@staticmethod def __init__(self, *, name: str):
def from_csv(row: List[str]): self.name = name # Primary key
team: Team = Team()
team.name = str(row[0]) @classmethod
return team def from_csv(cls, row: List[str]):
db_team: DbTeam = cls(name=str(row[0]))
return db_team
name: Mapped[str] = mapped_column(String(32), primary_key=True) name: Mapped[str] = mapped_column(String(32), primary_key=True)

View File

@ -0,0 +1,32 @@
from typing import Any, List
from sqlalchemy import Boolean, String
from sqlalchemy.orm import Mapped, mapped_column
from formula10 import db
class DbUser(db.Model):
"""
A user that can guess races (name only).
"""
__tablename__ = "user"
__csv_header__ = ["name", "enabled"]
def __init__(self, *, name: str, enabled: bool):
self.name = name # Primary key
self.enabled = enabled
@classmethod
def from_csv(cls, row: List[str]):
db_user: DbUser = cls(name=str(row[0]), enabled=bool(row[1]))
return db_user
def to_csv(self) -> List[Any]:
return [
self.name,
self.enabled
]
name: Mapped[str] = mapped_column(String(32), primary_key=True)
enabled: Mapped[bool] = mapped_column(Boolean)

View File

@ -1,60 +0,0 @@
import json
from typing import Any, List
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from formula10.database.model.user import User
from formula10.database.model.driver import Driver
from formula10 import db
class PodiumDrivers(db.Model):
"""
A guessed list of each driver that will reach at least a single podium.
"""
__tablename__ = "podiumdrivers"
__allow_unmapped__ = True
__csv_header__ = ["user_name", "podium_driver_names_json"]
def __init__(self, user_name: str):
self.user_name = user_name
@staticmethod
def from_csv(row: List[str]):
podium_drivers: PodiumDrivers = PodiumDrivers(str(row[0]))
podium_drivers.podium_driver_names_json = str(row[1])
return podium_drivers
def to_csv(self) -> List[Any]:
return [
self.user_name,
self.podium_driver_names_json
]
user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True)
podium_driver_names_json: Mapped[str] = mapped_column(String(1024), nullable=True)
@property
def podium_driver_names(self) -> List[str]:
return json.loads(self.podium_driver_names_json)
@podium_driver_names.setter
def podium_driver_names(self, new_podium_driver_names: List[str]):
self.podium_driver_names_json = json.dumps(new_podium_driver_names)
# Relationships
user: Mapped["User"] = relationship("User", foreign_keys=[user_name])
_podium_drivers: List[Driver] | None = None
@property
def podium_drivers(self) -> List[Driver]:
if self._podium_drivers is None:
self._podium_drivers = list()
for driver_name in self.podium_driver_names:
driver: Driver | None = db.session.query(Driver).filter_by(name=driver_name).first()
if driver is None:
raise Exception(f"Error: Couldn't find driver with id {driver_name}")
self._podium_drivers.append(driver)
return self._podium_drivers

View File

@ -1,46 +0,0 @@
from typing import Any, List
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from formula10.database.model.user import User
from formula10.database.model.race import Race
from formula10.database.model.driver import Driver
from formula10 import db
class RaceGuess(db.Model):
"""
A guess a user made for a race.
It stores the corresponding race and the guessed drivers for PXX and DNF.
"""
__tablename__ = "raceguess"
__csv_header__ = ["user_name", "race_name", "pxx_driver_name", "dnf_driver_name"]
def __init__(self, user_name: str, race_name: str):
self.user_name = user_name # Primary key
self.race_name = race_name # Primery key
@staticmethod
def from_csv(row: List[str]):
race_guess: RaceGuess = RaceGuess(str(row[0]), str(row[1]))
race_guess.pxx_driver_name = str(row[2])
race_guess.dnf_driver_name = str(row[3])
return race_guess
def to_csv(self) -> List[Any]:
return [
self.user_name,
self.race_name,
self.pxx_driver_name,
self.dnf_driver_name
]
user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True)
race_name: Mapped[str] = mapped_column(ForeignKey("race.name"), primary_key=True)
pxx_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name"), nullable=True)
dnf_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name"), nullable=True)
# Relationships
user: Mapped["User"] = relationship("User", foreign_keys=[user_name])
race: Mapped["Race"] = relationship("Race", foreign_keys=[race_name])
pxx: Mapped["Driver"] = relationship("Driver", foreign_keys=[pxx_driver_name])
dnf: Mapped["Driver"] = relationship("Driver", foreign_keys=[dnf_driver_name])

View File

@ -1,171 +0,0 @@
import json
from typing import Any, Dict, List
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from formula10.database.model.driver import Driver
from formula10.database.model.race import Race
from formula10 import db
class RaceResult(db.Model):
"""
The result of a past race.
It stores the corresponding race and dictionaries of place-/dnf-order and a list of drivers that are excluded from the standings for this race.
"""
__tablename__ = "raceresult"
__allow_unmapped__ = True # TODO: Used for json conversion, move this to some other class instead
__csv_header__ = ["race_name", "pxx_driver_names_json", "first_dnf_driver_names_json", "dnf_driver_names_json", "excluded_driver_names_json"]
def __init__(self, race_name: str):
self.race_name = race_name # Primary key
@staticmethod
def from_csv(row: List[str]):
race_result: RaceResult = RaceResult(str(row[0]))
race_result.pxx_driver_names_json = str(row[1])
race_result.first_dnf_driver_names_json = str(row[2])
race_result.dnf_driver_names_json = str(row[3])
race_result.excluded_driver_names_json = str(row[4])
return race_result
def to_csv(self) -> List[Any]:
return [
self.race_name,
self.pxx_driver_names_json,
self.first_dnf_driver_names_json,
self.dnf_driver_names_json,
self.excluded_driver_names_json
]
race_name: Mapped[str] = mapped_column(ForeignKey("race.name"), primary_key=True)
pxx_driver_names_json: Mapped[str] = mapped_column(String(1024), nullable=True)
first_dnf_driver_names_json: Mapped[str] = mapped_column(String(1024), nullable=True)
dnf_driver_names_json: Mapped[str] = mapped_column(String(1024), nullable=True)
excluded_driver_names_json: Mapped[str] = mapped_column(String(1024), nullable=True)
@property
def pxx_driver_names(self) -> Dict[str, str]:
return json.loads(self.pxx_driver_names_json)
@pxx_driver_names.setter
def pxx_driver_names(self, new_pxx_driver_names: Dict[str, str]):
self.pxx_driver_names_json = json.dumps(new_pxx_driver_names)
@property
def first_dnf_driver_names(self) -> List[str]:
return json.loads(self.first_dnf_driver_names_json)
@first_dnf_driver_names.setter
def first_dnf_driver_names(self, new_first_dnf_driver_names: List[str]):
self.first_dnf_driver_names_json = json.dumps(new_first_dnf_driver_names)
@property
def dnf_driver_names(self) -> List[str]:
return json.loads(self.dnf_driver_names_json)
@dnf_driver_names.setter
def dnf_driver_names(self, new_dnf_driver_names: List[str]):
self.dnf_driver_names_json = json.dumps(new_dnf_driver_names)
@property
def excluded_driver_names(self) -> List[str]:
return json.loads(self.excluded_driver_names_json)
@excluded_driver_names.setter
def excluded_driver_names(self, new_excluded_driver_names: List[str]):
self.excluded_driver_names_json = json.dumps(new_excluded_driver_names)
# Relationships
race: Mapped["Race"] = relationship("Race", foreign_keys=[race_name])
_pxx_drivers: Dict[str, Driver] | None = None
_first_dnf_drivers: List[Driver] | None = None
_dnf_drivers: List[Driver] | None = None
_excluded_drivers: List[Driver] | None = None
@property
def pxx_drivers(self) -> Dict[str, Driver]:
if self._pxx_drivers is None:
self._pxx_drivers = dict()
for position, driver_name in self.pxx_driver_names.items():
driver: Driver | None = db.session.query(Driver).filter_by(name=driver_name).first()
if driver is None:
raise Exception(f"Error: Couldn't find driver with id {driver_name}")
self._pxx_drivers[position] = driver
return self._pxx_drivers
def pxx_driver(self, offset: int = 0) -> Driver | None:
pxx_num: str = str(self.race.pxx + offset)
if pxx_num not in self.pxx_drivers:
raise Exception(f"Position {pxx_num} not found in RaceResult.pxx_drivers")
if self.pxx_drivers[pxx_num].name in self.excluded_driver_names:
none_driver: Driver | None = db.session.query(Driver).filter_by(name="None").first()
if none_driver is None:
raise Exception(f"NONE-driver not found in database")
return none_driver
return self.pxx_drivers[pxx_num]
def pxx_driver_position_string(self, driver_name: str) -> str:
for position, driver in self.pxx_driver_names.items():
if driver == driver_name and driver not in self.excluded_driver_names:
return f"P{position}"
return "NC"
@property
def all_positions(self) -> List[Driver]:
return [
self.pxx_drivers[str(position)] for position in range(1, 21)
]
@property
def first_dnf_drivers(self) -> List[Driver]:
if self._first_dnf_drivers is None:
self._first_dnf_drivers = list()
for driver_name in self.first_dnf_driver_names:
driver: Driver | None = db.session.query(Driver).filter_by(name=driver_name).first()
if driver is None:
raise Exception(f"Error: Couldn't find driver with id {driver_name}")
self._first_dnf_drivers.append(driver)
if len(self._first_dnf_drivers) == 0:
none_driver: Driver | None = db.session.query(Driver).filter_by(name="None").first()
if none_driver is None:
raise Exception("NONE-driver not found in database")
self._first_dnf_drivers.append(none_driver)
return self._first_dnf_drivers
@property
def dnf_drivers(self) -> List[Driver]:
if self._dnf_drivers is None:
self._dnf_drivers = list()
for driver_name in self.dnf_driver_names:
driver: Driver | None = db.session.query(Driver).filter_by(name=driver_name).first()
if driver is None:
raise Exception(f"Error: Couldn't find driver with id {driver_name}")
self._dnf_drivers.append(driver)
return self._dnf_drivers
@property
def excluded_drivers(self) -> List[Driver]:
if self._excluded_drivers is None:
self._excluded_drivers = list()
for driver_name in self.excluded_driver_names:
driver: Driver | None = db.session.query(Driver).filter_by(name=driver_name).first()
if driver is None:
raise Exception(f"Error: Couldn't find driver with id {driver_name}")
self._excluded_drivers.append(driver)
return self._excluded_drivers

View File

@ -1,81 +0,0 @@
from typing import Any, List
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from formula10.database.model.driver import Driver
from formula10.database.model.podium_drivers import PodiumDrivers
from formula10.database.model.team import Team
from formula10.database.model.team_winners import TeamWinners
from formula10.database.model.user import User
from formula10 import db
class SeasonGuess(db.Model):
"""
A collection of bonus guesses for the entire season.
"""
__tablename__ = "seasonguess"
__csv_header__ = ["user_name", "hot_take", "p2_team_name",
"overtake_driver_name", "dnf_driver_name", "gained_driver_name", "lost_driver_name",
"team_winners_id", "podium_drivers_id"]
def __init__(self, user_name: str, team_winners_user_name: str | None = None, podium_drivers_user_name: str | None = None):
self.user_name = user_name # Primary key
# Although this is the same username, handle separately, in case they don't exist in the database yet
if team_winners_user_name is not None:
if user_name != team_winners_user_name:
raise Exception(f"SeasonGuess for {user_name} was supplied TeamWinners for {team_winners_user_name}")
self.team_winners_id = team_winners_user_name
if podium_drivers_user_name is not None:
if user_name != podium_drivers_user_name:
raise Exception(f"SeasonGuess for {user_name} was supplied PodiumDrivers for {podium_drivers_user_name}")
self.podium_drivers_id = podium_drivers_user_name
@staticmethod
def from_csv(row: List[str]):
season_guess: SeasonGuess = SeasonGuess(str(row[0]), team_winners_user_name=str(row[7]), podium_drivers_user_name=str(row[8]))
season_guess.hot_take = str(row[1])
season_guess.p2_team_name = str(row[2])
season_guess.overtake_driver_name = str(row[3])
season_guess.dnf_driver_name = str(row[4])
season_guess.gained_driver_name = str(row[5])
season_guess.lost_driver_name = str(row[6])
return season_guess
def to_csv(self) -> List[Any]:
return [
self.user_name,
self.hot_take,
self.p2_team_name,
self.overtake_driver_name,
self.dnf_driver_name,
self.gained_driver_name,
self.lost_driver_name,
self.team_winners_id,
self.podium_drivers_id
]
user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True)
hot_take: Mapped[str] = mapped_column(String(512), nullable=True)
p2_team_name: Mapped[str] = mapped_column(ForeignKey("team.name"), nullable=True)
overtake_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name"), nullable=True)
dnf_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name"), nullable=True)
gained_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name"), nullable=True)
lost_driver_name: Mapped[str] = mapped_column(ForeignKey("driver.name"), nullable=True)
team_winners_id: Mapped[str] = mapped_column(ForeignKey("teamwinners.user_name"))
podium_drivers_id: Mapped[str] = mapped_column(ForeignKey("podiumdrivers.user_name"))
# Relationships
user: Mapped["User"] = relationship("User", foreign_keys=[user_name])
p2_team: Mapped["Team"] = relationship("Team", foreign_keys=[p2_team_name])
overtake_driver: Mapped["Driver"] = relationship("Driver", foreign_keys=[overtake_driver_name])
dnf_driver: Mapped["Driver"] = relationship("Driver", foreign_keys=[dnf_driver_name])
gained_driver: Mapped["Driver"] = relationship("Driver", foreign_keys=[gained_driver_name])
lost_driver: Mapped["Driver"] = relationship("Driver", foreign_keys=[lost_driver_name])
team_winners: Mapped["TeamWinners"] = relationship("TeamWinners", foreign_keys=[team_winners_id])
podium_drivers: Mapped["PodiumDrivers"] = relationship("PodiumDrivers", foreign_keys=[podium_drivers_id])

View File

@ -1,59 +0,0 @@
import json
from typing import Any, List
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from formula10.database.model.driver import Driver
from formula10.database.model.user import User
from formula10 import db
class TeamWinners(db.Model):
"""
A guessed list of each best driver per team.
"""
__tablename__ = "teamwinners"
__allow_unmapped__ = True
__csv_header__ = ["user_name", "teamwinner_driver_names_json"]
def __init__(self, user_name: str):
self.user_name = user_name # Primary key
@staticmethod
def from_csv(row: List[str]):
team_winners: TeamWinners = TeamWinners(str(row[0]))
team_winners.teamwinner_driver_names_json = str(row[1])
return team_winners
def to_csv(self) -> List[Any]:
return [
self.user_name,
self.teamwinner_driver_names_json
]
user_name: Mapped[str] = mapped_column(ForeignKey("user.name"), primary_key=True)
teamwinner_driver_names_json: Mapped[str] = mapped_column(String(1024), nullable=True)
@property
def teamwinner_driver_names(self) -> List[str]:
return json.loads(self.teamwinner_driver_names_json)
@teamwinner_driver_names.setter
def teamwinner_driver_names(self, new_teamwinner_driver_names: List[str]):
self.teamwinner_driver_names_json = json.dumps(new_teamwinner_driver_names)
# Relationships
user: Mapped["User"] = relationship("User", foreign_keys=[user_name])
_teamwinner_drivers: List[Driver] | None = None
@property
def teamwinners(self) -> List[Driver]:
if self._teamwinner_drivers is None:
self._teamwinner_drivers = list()
for driver_name in self.teamwinner_driver_names:
driver: Driver | None = db.session.query(Driver).filter_by(name=driver_name).first()
if driver is None:
raise Exception(f"Error: Couldn't find driver with id {driver_name}")
self._teamwinner_drivers.append(driver)
return self._teamwinner_drivers

View File

@ -1,32 +0,0 @@
from typing import Any, List
from urllib.parse import quote
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from formula10 import db
class User(db.Model):
"""
A user that can guess races (name only).
"""
__tablename__ = "user"
__csv_header__ = ["name"]
def __init__(self, name: str):
self.name = name # Primary key
@staticmethod
def from_csv(row: List[str]):
user: User = User(str(row[0]))
return user
def to_csv(self) -> List[Any]:
return [
self.name
]
@property
def name_sanitized(self) -> str:
return quote(self.name)
name: Mapped[str] = mapped_column(String(32), primary_key=True)

View File

@ -1,32 +1,31 @@
import json
from typing import Dict, List, cast from typing import Dict, List, cast
from urllib.parse import quote from urllib.parse import quote
from flask import redirect from flask import redirect
from werkzeug import Response from werkzeug import Response
from formula10.database.common_query_util import race_has_result, user_exists from formula10.database.common_queries import race_has_result, user_exists_and_disabled, user_exists_and_enabled
from formula10.database.model.podium_drivers import PodiumDrivers from formula10.database.model.db_race_guess import DbRaceGuess
from formula10.database.model.race_guess import RaceGuess from formula10.database.model.db_race_result import DbRaceResult
from formula10.database.model.race_result import RaceResult from formula10.database.model.db_season_guess import DbSeasonGuess
from formula10.database.model.season_guess import SeasonGuess from formula10.database.model.db_user import DbUser
from formula10.database.model.team_winners import TeamWinners from formula10.database.validation import any_is_none, positions_are_contiguous
from formula10.database.model.user import User
from formula10.database.validation_util import any_is_none, positions_are_contiguous
from formula10 import db from formula10 import db
def find_or_create_race_guess(user_name: str, race_name: str) -> RaceGuess: def find_or_create_race_guess(user_name: str, race_name: str) -> DbRaceGuess:
# There can be a single RaceGuess at most, since (user_name, race_name) is the composite primary key # There can be a single RaceGuess at most, since (user_name, race_name) is the composite primary key
race_guess: RaceGuess | None = db.session.query(RaceGuess).filter_by(user_name=user_name, race_name=race_name).first() race_guess: DbRaceGuess | None = db.session.query(DbRaceGuess).filter_by(user_name=user_name, race_name=race_name).first()
if race_guess is not None: if race_guess is not None:
return race_guess return race_guess
# Insert a new RaceGuess # Insert a new RaceGuess
race_guess = RaceGuess(user_name=user_name, race_name=race_name) race_guess = DbRaceGuess(user_name=user_name, race_name=race_name, pxx_driver_name="TEMP", dnf_driver_name="TEMP")
db.session.add(race_guess) db.session.add(race_guess)
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_guess = db.session.query(RaceGuess).filter_by(user_name=user_name, race_name=race_name).first() race_guess = db.session.query(DbRaceGuess).filter_by(user_name=user_name, race_name=race_name).first()
if race_guess is None: if race_guess is None:
raise Exception("Failed adding RaceGuess to the database") raise Exception("Failed adding RaceGuess to the database")
@ -46,7 +45,7 @@ def update_race_guess(race_name: str, user_name: str, pxx_select: str | None, dn
if race_has_result(race_name): if race_has_result(race_name):
return redirect(f"/race/{quote(user_name)}") return redirect(f"/race/{quote(user_name)}")
race_guess: RaceGuess = find_or_create_race_guess(user_name, race_name) race_guess: DbRaceGuess = find_or_create_race_guess(user_name, race_name)
race_guess.pxx_driver_name = pxx_driver_name race_guess.pxx_driver_name = pxx_driver_name
race_guess.dnf_driver_name = dnf_driver_name race_guess.dnf_driver_name = dnf_driver_name
@ -60,70 +59,25 @@ def delete_race_guess(race_name: str, user_name: str) -> Response:
if race_has_result(race_name): if race_has_result(race_name):
return redirect(f"/race/{quote(user_name)}") return redirect(f"/race/{quote(user_name)}")
db.session.query(RaceGuess).filter_by(race_name=race_name, user_name=user_name).delete() db.session.query(DbRaceGuess).filter_by(race_name=race_name, user_name=user_name).delete()
db.session.commit() db.session.commit()
return redirect("/race/Everyone") return redirect("/race/Everyone")
def find_or_create_team_winners(user_name: str) -> TeamWinners: def find_or_create_season_guess(user_name: str) -> DbSeasonGuess:
# There can be a single TeamWinners at most, since user_name is the primary key
team_winners: TeamWinners | None = db.session.query(TeamWinners).filter_by(user_name=user_name).first()
if team_winners is not None:
return team_winners
team_winners = TeamWinners(user_name=user_name)
db.session.add(team_winners)
db.session.commit()
# Double check if database insertion worked and obtain any values set by the database
team_winners = db.session.query(TeamWinners).filter_by(user_name=user_name).first()
if team_winners is None:
raise Exception("Failed adding TeamWinners to the database")
return team_winners
def find_or_create_podium_drivers(user_name: str) -> PodiumDrivers:
# There can be a single PodiumDrivers at most, since user_name is the primary key
podium_drivers: PodiumDrivers | None = db.session.query(PodiumDrivers).filter_by(user_name=user_name).first()
if podium_drivers is not None:
return podium_drivers
podium_drivers = PodiumDrivers(user_name=user_name)
db.session.add(podium_drivers)
db.session.commit()
# Double check if database insertion worked and obtain any values set by the database
podium_drivers = db.session.query(PodiumDrivers).filter_by(user_name=user_name).first()
if podium_drivers is None:
raise Exception("Failed adding PodiumDrivers to the database")
return podium_drivers
def find_or_create_season_guess(user_name: str) -> SeasonGuess:
# There can be a single SeasonGuess at most, since user_name is the primary key # There can be a single SeasonGuess at most, since user_name is the primary key
season_guess: SeasonGuess | None = db.session.query(SeasonGuess).filter_by(user_name=user_name).first() season_guess: DbSeasonGuess | None = db.session.query(DbSeasonGuess).filter_by(user_name=user_name).first()
if season_guess is not None: if season_guess is not None:
# There can't be more than a single one, since both also use user_name as primary key
if db.session.query(TeamWinners).filter_by(user_name=user_name).first() is None:
raise Exception(f"SeasonGuess for {user_name} is missing associated TeamWinners")
if db.session.query(PodiumDrivers).filter_by(user_name=user_name).first() is None:
raise Exception(f"SeasonGuess for {user_name} is missing associated PodiumDrivers")
return season_guess return season_guess
# Insert a new SeasonGuess # Insert a new SeasonGuess
team_winners: TeamWinners = find_or_create_team_winners(user_name) season_guess = DbSeasonGuess(user_name=user_name, team_winners_driver_names_json=json.dumps(["TEMP"]), podium_drivers_driver_names_json=json.dumps(["TEMP"]))
podium_drivers: PodiumDrivers = find_or_create_podium_drivers(user_name)
season_guess = SeasonGuess(user_name=user_name, team_winners_user_name=team_winners.user_name, podium_drivers_user_name=podium_drivers.user_name)
db.session.add(season_guess) db.session.add(season_guess)
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
season_guess = db.session.query(SeasonGuess).filter_by(user_name=user_name).first() season_guess = db.session.query(DbSeasonGuess).filter_by(user_name=user_name).first()
if season_guess is None: if season_guess is None:
raise Exception("Failed adding SeasonGuess to the database") raise Exception("Failed adding SeasonGuess to the database")
@ -133,33 +87,37 @@ def find_or_create_season_guess(user_name: str) -> SeasonGuess:
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.
season_guess: SeasonGuess = find_or_create_season_guess(user_name) season_guess: DbSeasonGuess = find_or_create_season_guess(user_name)
season_guess.hot_take = guesses[0] # type: ignore season_guess.hot_take = guesses[0] # type: ignore
season_guess.p2_team_name = guesses[1] # type: ignore season_guess.p2_team_name = guesses[1] # type: ignore
season_guess.overtake_driver_name = guesses[2] # type: ignore season_guess.overtake_driver_name = guesses[2] # type: ignore
season_guess.dnf_driver_name = guesses[3] # type: ignore season_guess.dnf_driver_name = guesses[3] # type: ignore
season_guess.gained_driver_name = guesses[4] # type: ignore season_guess.gained_driver_name = guesses[4] # type: ignore
season_guess.lost_driver_name = guesses[5] # type: ignore season_guess.lost_driver_name = guesses[5] # type: ignore
season_guess.team_winners.teamwinner_driver_names = team_winner_guesses # type: ignore season_guess.team_winners_driver_names_json = json.dumps(team_winner_guesses)
season_guess.podium_drivers.podium_driver_names = podium_driver_guesses season_guess.podium_drivers_driver_names_json = json.dumps(podium_driver_guesses)
db.session.commit() db.session.commit()
return redirect(f"/season/Everyone") return redirect(f"/season/Everyone")
def find_or_create_race_result(race_name: str) -> RaceResult: def find_or_create_race_result(race_name: str) -> 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: RaceResult | None = db.session.query(RaceResult).filter_by(race_name=race_name).first() race_result: DbRaceResult | None = db.session.query(DbRaceResult).filter_by(race_name=race_name).first()
if race_result is not None: if race_result is not None:
return race_result return race_result
race_result = RaceResult(race_name=race_name) race_result = DbRaceResult(race_name=race_name,
pxx_driver_names_json=json.dumps(["TEMP"]),
first_dnf_driver_names_json=json.dumps(["TEMP"]),
dnf_driver_names_json=json.dumps(["TEMP"]),
excluded_driver_names_json=json.dumps(["TEMP"]))
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(RaceResult).filter_by(race_name=race_name).first() race_result = db.session.query(DbRaceResult).filter_by(race_name=race_name).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")
@ -185,11 +143,15 @@ def update_race_result(race_name: str, pxx_driver_names_list: List[str], first_d
if driver_name not in dnf_driver_names_list: if driver_name not in dnf_driver_names_list:
dnf_driver_names_list.append(driver_name) dnf_driver_names_list.append(driver_name)
race_result: RaceResult = find_or_create_race_result(race_name) # There can't be dnfs but no initial dnfs
race_result.pxx_driver_names = pxx_driver_names if len(dnf_driver_names_list) > 0 and len(first_dnf_driver_names_list) == 0:
race_result.first_dnf_driver_names = first_dnf_driver_names_list return redirect(f"/result/{quote(race_name)}")
race_result.dnf_driver_names = dnf_driver_names_list
race_result.excluded_driver_names = excluded_driver_names_list race_result: DbRaceResult = find_or_create_race_result(race_name)
race_result.pxx_driver_names_json = json.dumps(pxx_driver_names)
race_result.first_dnf_driver_names_json = json.dumps(first_dnf_driver_names_list)
race_result.dnf_driver_names_json = json.dumps(dnf_driver_names_list)
race_result.excluded_driver_names_json = json.dumps(excluded_driver_names_list)
db.session.commit() db.session.commit()
@ -207,21 +169,32 @@ def update_user(user_name: str | None, add: bool = False, delete: bool = False)
return redirect("/user") return redirect("/user")
if add: if add:
if user_exists(user_name): if user_exists_and_enabled(user_name):
return redirect("/user") return redirect("/user")
user: User = User(name=user_name) elif user_exists_and_disabled(user_name):
db.session.add(user) disabled_user: DbUser | None = db.session.query(DbUser).filter_by(name=user_name, enabled=False).first()
if disabled_user is None:
raise Exception("update_user couldn't reenable user")
disabled_user.enabled = True
else:
user: DbUser = DbUser(name=user_name, enabled=True)
db.session.add(user)
db.session.commit() db.session.commit()
return redirect("/user") return redirect("/user")
if delete: if delete:
if not user_exists(user_name): if user_exists_and_enabled(user_name):
return redirect("/user") enabled_user: DbUser | None = db.session.query(DbUser).filter_by(name=user_name, enabled=True).first()
if enabled_user is None:
raise Exception("update_user couldn't disable user")
db.session.query(User).filter_by(name=user_name).delete() enabled_user.enabled = False
db.session.commit() db.session.commit()
return redirect("/user") return redirect("/user")

View File

@ -22,7 +22,7 @@ def positions_are_contiguous(positions: List[str]) -> bool:
return positions_sorted[0] + len(positions_sorted) - 1 == positions_sorted[-1] return positions_sorted[0] + len(positions_sorted) - 1 == positions_sorted[-1]
def find_first_or_none(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T | None: def find_first_else_none(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T | None:
""" """
Finds the first element in a sequence matching a predicate. Finds the first element in a sequence matching a predicate.
Returns None if no element is found. Returns None if no element is found.
@ -30,7 +30,13 @@ def find_first_or_none(predicate: Callable[[_T], bool], iterable: Iterable[_T])
return next(filter(predicate, iterable), None) return next(filter(predicate, iterable), None)
def find_multiple(predicate: Callable[[_T], bool], iterable: Iterable[_T], count: int = 0) -> List[_T]: def find_multiple(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> List[_T]:
filtered = list(filter(predicate, iterable))
return filtered
def find_multiple_strict(predicate: Callable[[_T], bool], iterable: Iterable[_T], count: int = 0) -> List[_T]:
""" """
Finds <count> elements in a sequence matching a predicate (finds all if <count> is 0). Finds <count> elements in a sequence matching a predicate (finds all if <count> is 0).
Throws exception if more/fewer elements were found than specified. Throws exception if more/fewer elements were found than specified.
@ -43,7 +49,7 @@ def find_multiple(predicate: Callable[[_T], bool], iterable: Iterable[_T], count
return filtered return filtered
def find_single(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.
Throws exception if more/less than a single element is found. Throws exception if more/less than a single element is found.
@ -56,7 +62,7 @@ def find_single(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T:
return filtered[0] return filtered[0]
def find_single_or_none(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T | None: def find_single_or_none_strict(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T | None:
""" """
Find a single element in a sequence matching a predicate if it exists. Find a single element in a sequence matching a predicate if it exists.
Only throws exception if more than a single element is found. Only throws exception if more than a single element is found.

View File

@ -0,0 +1,44 @@
from urllib.parse import quote
from formula10.database.model.db_driver import DbDriver
from formula10.frontend.model.team import NONE_TEAM, Team
class Driver():
@classmethod
def from_db_driver(cls, db_driver: DbDriver):
driver: Driver = cls()
driver.name = db_driver.name
driver.abbr = db_driver.abbr
driver.country = db_driver.country_code
driver.team = Team.from_db_team(db_driver.team)
return driver
def to_db_driver(self) -> DbDriver:
db_driver: DbDriver = DbDriver(name=self.name)
db_driver.abbr = self.abbr
db_driver.country_code = self.country
db_driver.team_name = self.team.name
return db_driver
def __eq__(self, __value: object) -> bool:
if isinstance(__value, Driver):
return self.name == __value.name
return NotImplemented
name: str
abbr: str
country: str
team: Team
@property
def name_sanitized(self) -> str:
return quote(self.name)
NONE_DRIVER: Driver = Driver()
NONE_DRIVER.name = "None"
NONE_DRIVER.abbr = "None"
NONE_DRIVER.country = "NO"
NONE_DRIVER.team = NONE_TEAM

View File

@ -0,0 +1,37 @@
from datetime import datetime
from urllib.parse import quote
from formula10.database.model.db_race import DbRace
class Race():
@classmethod
def from_db_race(cls, db_race: DbRace):
race: Race = cls()
race.name = db_race.name
race.number = db_race.number
race.date = db_race.date
race.place_to_guess = db_race.pxx
return race
def to_db_race(self) -> DbRace:
db_race: DbRace = DbRace(name=self.name,
number=self.number,
date=self.date,
pxx=self.place_to_guess)
return db_race
def __eq__(self, __value: object) -> bool:
if isinstance(__value, Race):
return self.name == __value.name
return NotImplemented
name: str
number: int
date: datetime
place_to_guess: int
@property
def name_sanitized(self) -> str:
return quote(self.name)

View File

@ -0,0 +1,33 @@
from formula10.database.model.db_race_guess import DbRaceGuess
from formula10.frontend.model.driver import Driver
from formula10.frontend.model.race import Race
from formula10.frontend.model.user import User
class RaceGuess():
@classmethod
def from_db_race_guess(cls, db_race_guess: DbRaceGuess):
race_guess: RaceGuess = cls()
race_guess.user = User.from_db_user(db_race_guess.user)
race_guess.race = Race.from_db_race(db_race_guess.race)
race_guess.pxx_guess = Driver.from_db_driver(db_race_guess.pxx)
race_guess.dnf_guess = Driver.from_db_driver(db_race_guess.dnf)
return race_guess
def to_db_race_guess(self) -> DbRaceGuess:
db_race_guess: DbRaceGuess = DbRaceGuess(user_name=self.user.name,
race_name=self.race.name,
pxx_driver_name=self.pxx_guess.name,
dnf_driver_name=self.dnf_guess.name)
return db_race_guess
def __eq__(self, __value: object) -> bool:
if isinstance(__value, RaceGuess):
return self.user == __value.user and self.race == __value.race
return NotImplemented
user: User
race: Race
pxx_guess: Driver
dnf_guess: Driver

View File

@ -0,0 +1,148 @@
import json
from typing import Dict, List
from formula10.database.common_queries import find_single_driver_strict
from formula10.database.model.db_race_result import DbRaceResult
from formula10.frontend.model.driver import NONE_DRIVER, Driver
from formula10.frontend.model.race import Race
class RaceResult:
@classmethod
def from_db_race_result(cls, db_race_result: DbRaceResult):
race_result: RaceResult = cls()
race_result.race = Race.from_db_race(db_race_result.race)
# Deserialize from json
standing: Dict[str, str] = json.loads(db_race_result.pxx_driver_names_json)
initial_dnf: List[str] = json.loads(db_race_result.first_dnf_driver_names_json)
all_dnfs: List[str] = json.loads(db_race_result.dnf_driver_names_json)
standing_exclusions: List[str] = json.loads(db_race_result.excluded_driver_names_json)
# Populate relationships
race_result.standing = {
position: Driver.from_db_driver(find_single_driver_strict(driver_name))
for position, driver_name in standing.items()
}
race_result.initial_dnf = [
Driver.from_db_driver(find_single_driver_strict(driver_name))
for driver_name in initial_dnf
]
race_result.all_dnfs = [
Driver.from_db_driver(find_single_driver_strict(driver_name))
for driver_name in all_dnfs
]
race_result.standing_exclusions = [
Driver.from_db_driver(find_single_driver_strict(driver_name))
for driver_name in standing_exclusions
]
return race_result
def to_db_race_result(self) -> DbRaceResult:
# "Unpopulate" relationships, remove none driver
standing: Dict[str, str] = {
position: driver.name for position, driver in self.standing.items()
}
initial_dnf: List[str] = [
driver.name for driver in self.initial_dnf if driver
]
all_dnfs: List[str] = [
driver.name for driver in self.all_dnfs if driver
]
standing_exclusions: List[str] = [
driver.name for driver in self.standing_exclusions if driver
]
# Serialize to json
db_race_result: DbRaceResult = DbRaceResult(race_name=self.race.name,
pxx_driver_names_json=json.dumps(standing),
first_dnf_driver_names_json=json.dumps(initial_dnf),
dnf_driver_names_json=json.dumps(all_dnfs),
excluded_driver_names_json=json.dumps(standing_exclusions))
return db_race_result
def __eq__(self, __value: object) -> bool:
if isinstance(__value, RaceResult):
return self.race == __value.race
return NotImplemented
race: Race
standing: Dict[str, Driver]
initial_dnf: List[Driver]
all_dnfs: List[Driver]
standing_exclusions: List[Driver]
def offset_from_place_to_guess(self, offset: int, respect_nc:bool = True) -> Driver:
position: str = str(self.race.place_to_guess + offset)
if position not in self.standing:
raise Exception(f"Position {position} not found in RaceResult.standing")
if self.standing[position] in self.standing_exclusions and respect_nc:
return NONE_DRIVER
return self.standing[position]
def driver_standing_position_string(self, driver: Driver) -> str:
if driver == NONE_DRIVER:
return ""
for position, _driver in self.standing.items():
if driver == _driver and driver not in self.standing_exclusions:
return f" (P{position})"
return " (NC)"
def driver_standing_points_string(self, driver: Driver) -> str:
points_strings: Dict[int, str] = {
0: "10 Points",
1: "6 Points",
2: "3 Points",
3: "1 Points"
}
if driver == NONE_DRIVER:
if self.standing[str(self.race.place_to_guess)] in self.standing_exclusions:
return "10 Points"
else:
return "0 Points"
for position, _driver in self.standing.items():
if driver == _driver and driver not in self.standing_exclusions:
position_offset: int = abs(self.race.place_to_guess - int(position))
if position_offset in points_strings:
return points_strings[position_offset]
else:
return "0 Points"
raise Exception(f"Could not get points string for driver {driver.name}")
def driver_dnf_points_string(self, driver: Driver) -> str:
if driver == NONE_DRIVER:
if len(self.initial_dnf) == 0:
return "10 Points"
else:
return "0 Points"
if driver in self.initial_dnf:
return "10 Points"
else:
return "0 Points"
def ordered_standing_list(self) -> List[Driver]:
return [
self.standing[str(position)] for position in range(1, 21)
]
def initial_dnf_string(self) -> str:
if len(self.initial_dnf) == 0:
return NONE_DRIVER.name
dnf_string: str = ""
for driver in self.initial_dnf:
dnf_string += f"{driver.abbr} "
return dnf_string[0:len(dnf_string)-1] # Remove last space

View File

@ -0,0 +1,73 @@
import json
from typing import List
from formula10.database.common_queries import find_single_driver_strict
from formula10.database.model.db_season_guess import DbSeasonGuess
from formula10.frontend.model.driver import Driver
from formula10.frontend.model.team import Team
from formula10.frontend.model.user import User
class SeasonGuess():
@classmethod
def from_db_season_guess(cls, db_season_guess: DbSeasonGuess):
season_guess: SeasonGuess = cls()
season_guess.user = User.from_db_user(db_season_guess.user)
season_guess.hot_take = db_season_guess.hot_take if db_season_guess.hot_take is not None else None
season_guess.p2_wcc = Team.from_db_team(db_season_guess.p2_team) if db_season_guess.p2_team is not None else None
season_guess.most_overtakes = Driver.from_db_driver(db_season_guess.overtake_driver) if db_season_guess.overtake_driver is not None else None
season_guess.most_dnfs = Driver.from_db_driver(db_season_guess.dnf_driver) if db_season_guess.dnf_driver is not None else None
season_guess.most_wdc_gained = Driver.from_db_driver(db_season_guess.gained_driver) if db_season_guess.gained_driver is not None else None
season_guess.most_wdc_lost = Driver.from_db_driver(db_season_guess.lost_driver) if db_season_guess.lost_driver is not None else None
# Deserialize from json
team_winners: List[str | None] = json.loads(db_season_guess.team_winners_driver_names_json)
podiums: List[str] = json.loads(db_season_guess.podium_drivers_driver_names_json)
# Populate relationships
season_guess.team_winners = [
Driver.from_db_driver(find_single_driver_strict(driver_name)) if driver_name is not None else None
for driver_name in team_winners
]
season_guess.podiums = [
Driver.from_db_driver(find_single_driver_strict(driver_name))
for driver_name in podiums
]
return season_guess
def to_db_season_guess(self):
# "Unpopulate" relationships
team_winners: List[str | None] = [
driver.name if driver is not None else None
for driver in self.team_winners
]
podiums: List[str] = [
driver.name for driver in self.podiums
]
# Serialize to json
db_season_guess: DbSeasonGuess = DbSeasonGuess(user_name=self.user.name,
team_winners_driver_names_json=json.dumps(team_winners),
podium_drivers_driver_names_json=json.dumps(podiums))
db_season_guess.user_name = self.user.name
db_season_guess.hot_take = self.hot_take
db_season_guess.p2_team_name = self.p2_wcc.name if self.p2_wcc is not None else None
db_season_guess.overtake_driver_name = self.most_overtakes.name if self.most_overtakes is not None else None
db_season_guess.dnf_driver_name = self.most_dnfs.name if self.most_dnfs is not None else None
db_season_guess.gained_driver_name = self.most_wdc_gained.name if self.most_wdc_gained is not None else None
db_season_guess.lost_driver_name = self.most_wdc_lost.name if self.most_wdc_lost is not None else None
return db_season_guess
user: User
hot_take: str | None
p2_wcc: Team | None
most_overtakes: Driver | None
most_dnfs: Driver | None
most_wdc_gained: Driver | None
most_wdc_lost: Driver | None
team_winners: List[Driver | None]
podiums: List[Driver]
def hot_take_string(self) -> str:
return self.hot_take if self.hot_take is not None else ""

View File

@ -0,0 +1,30 @@
from urllib.parse import quote
from formula10.database.model.db_team import DbTeam
class Team():
@classmethod
def from_db_team(cls, db_team: DbTeam):
team: Team = cls()
team.name = db_team.name
return team
def to_db_team(self) -> DbTeam:
db_team: DbTeam = DbTeam(name=self.name)
return db_team
def __eq__(self, __value: object) -> bool:
if isinstance(__value, Team):
return self.name == __value.name
return NotImplemented
name: str
@property
def name_sanitized(self) -> str:
return quote(self.name)
NONE_TEAM: Team = Team()
NONE_TEAM.name = "None"

View File

@ -0,0 +1,29 @@
from urllib.parse import quote
from formula10.database.model.db_user import DbUser
class User():
@classmethod
def from_db_user(cls, db_user: DbUser):
user: User = cls()
user.name = db_user.name
user.enabled = db_user.enabled
return user
def to_db_user(self) -> DbUser:
db_user: DbUser = DbUser(name=self.name, enabled=self.enabled)
return db_user
def __eq__(self, __value: object) -> bool:
if isinstance(__value, User):
return self.name == __value.name
return NotImplemented
name: str
enabled: bool
@property
def name_sanitized(self) -> str:
return quote(self.name)

View File

@ -1,18 +1,24 @@
from typing import List, Callable, Dict, overload from typing import List, Callable, Dict, overload
from sqlalchemy import desc from sqlalchemy import desc
from formula10.database.model.driver import Driver from formula10.database.model.db_driver import DbDriver
from formula10.database.model.race import Race from formula10.database.model.db_race import DbRace
from formula10.database.model.race_guess import RaceGuess from formula10.database.model.db_race_guess import DbRaceGuess
from formula10.database.model.race_result import RaceResult from formula10.database.model.db_race_result import DbRaceResult
from formula10.database.model.season_guess import SeasonGuess from formula10.database.model.db_season_guess import DbSeasonGuess
from formula10.database.model.team import Team from formula10.database.model.db_team import DbTeam
from formula10.database.model.user import User from formula10.database.model.db_user import DbUser
from formula10.database.validation_util import find_first_or_none, find_multiple, find_single, find_single_or_none from formula10.frontend.model.driver import NONE_DRIVER, Driver
from formula10.frontend.model.race import Race
from formula10.frontend.model.race_guess import RaceGuess
from formula10.frontend.model.race_result import RaceResult
from formula10.frontend.model.season_guess import SeasonGuess
from formula10.frontend.model.team import NONE_TEAM, Team
from formula10.frontend.model.user import User
from formula10.database.validation import find_first_else_none, find_multiple_strict, find_single_strict, find_single_or_none_strict
from formula10 import db from formula10 import db
# This could also be moved to database_utils (at least partially), but I though the template should cache the database responses
class TemplateModel: class TemplateModel:
""" """
This class bundles all data required from inside a template. This class bundles all data required from inside a template.
@ -26,15 +32,42 @@ 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
_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 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 users in the database.
""" """
if self._all_users is None: if self._all_users is None:
self._all_users = db.session.query(User).all() self._all_users = [
User.from_db_user(db_user)
for db_user in db.session.query(DbUser).filter_by(enabled=True).all()
]
return self._all_users return self._all_users
def all_users_or_active_user(self) -> List[User]:
if self.active_user is not None:
return [self.active_user]
return self.all_users()
@overload @overload
def user_by(self, *, user_name: str) -> User: def user_by(self, *, user_name: str) -> User:
""" """
@ -57,14 +90,17 @@ class TemplateModel:
return None return None
predicate: Callable[[User], bool] = lambda user: user.name == user_name predicate: Callable[[User], bool] = lambda user: user.name == user_name
return find_single(predicate, self.all_users()) return find_single_strict(predicate, self.all_users())
def all_race_results(self) -> List[RaceResult]: def all_race_results(self) -> List[RaceResult]:
""" """
Returns a list of all race results in the database, in descending order (most recent first). Returns a list of all race results in the database, in descending order (most recent first).
""" """
if self._all_race_results is None: if self._all_race_results is None:
self._all_race_results = db.session.query(RaceResult).join(RaceResult.race).order_by(desc(Race.number)).all() 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 return self._all_race_results
@ -73,14 +109,17 @@ class TemplateModel:
Tries to obtain the race result corresponding to a race name. Tries to obtain the race result corresponding to a race name.
""" """
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(predicate, self.all_race_results()) return find_single_or_none_strict(predicate, self.all_race_results())
def all_race_guesses(self) -> List[RaceGuess]: def all_race_guesses(self) -> List[RaceGuess]:
""" """
Returns a list of all race guesses in the database. Returns a list of all race guesses in the database.
""" """
if self._all_race_guesses is None: if self._all_race_guesses is None:
self._all_race_guesses = db.session.query(RaceGuess).all() 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 return self._all_race_guesses
@ -115,18 +154,18 @@ class TemplateModel:
def race_guesses_by(self, *, user_name: str | None = None, race_name: str | None = None) -> RaceGuess | List[RaceGuess] | Dict[str, Dict[str, RaceGuess]] | None: def race_guesses_by(self, *, user_name: str | None = None, race_name: str | None = None) -> RaceGuess | List[RaceGuess] | Dict[str, Dict[str, RaceGuess]] | None:
# List of all guesses by a single user # List of all guesses by a single user
if user_name is not None and race_name is None: if user_name is not None and race_name is None:
predicate: Callable[[RaceGuess], bool] = lambda guess: guess.user_name == user_name predicate: Callable[[RaceGuess], bool] = lambda guess: guess.user.name == user_name
return find_multiple(predicate, self.all_race_guesses()) return find_multiple_strict(predicate, self.all_race_guesses())
# List of all guesses for a single race # List of all guesses for a single race
if user_name is None and race_name is not None: if user_name is None and race_name is not None:
predicate: Callable[[RaceGuess], bool] = lambda guess: guess.race_name == race_name predicate: Callable[[RaceGuess], bool] = lambda guess: guess.race.name == race_name
return find_multiple(predicate, self.all_race_guesses()) return find_multiple_strict(predicate, self.all_race_guesses())
# Guess for a single race by a single user # Guess for a single race by a single user
if user_name is not None and race_name is not None: if user_name is not None and race_name is not None:
predicate: Callable[[RaceGuess], bool] = lambda guess: guess.user_name == user_name and guess.race_name == race_name predicate: Callable[[RaceGuess], bool] = lambda guess: guess.user.name == user_name and guess.race.name == race_name
return find_single_or_none(predicate, self.all_race_guesses()) return find_single_or_none_strict(predicate, self.all_race_guesses())
# Dict with all guesses # Dict with all guesses
if user_name is None and race_name is None: if user_name is None and race_name is None:
@ -134,10 +173,10 @@ class TemplateModel:
guess: RaceGuess guess: RaceGuess
for guess in self.all_race_guesses(): for guess in self.all_race_guesses():
if guess.race_name not in guesses_by: if guess.race.name not in guesses_by:
guesses_by[guess.race_name] = dict() guesses_by[guess.race.name] = dict()
guesses_by[guess.race_name][guess.user_name] = guess guesses_by[guess.race.name][guess.user.name] = guess
return guesses_by return guesses_by
@ -145,7 +184,10 @@ class TemplateModel:
def all_season_guesses(self) -> List[SeasonGuess]: def all_season_guesses(self) -> List[SeasonGuess]:
if self._all_season_guesses is None: if self._all_season_guesses is None:
self._all_season_guesses = db.session.query(SeasonGuess).all() 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 return self._all_season_guesses
@ -165,15 +207,15 @@ class TemplateModel:
def season_guesses_by(self, *, user_name: str | None = None) -> SeasonGuess | Dict[str, SeasonGuess] | None: def season_guesses_by(self, *, user_name: str | None = None) -> SeasonGuess | Dict[str, SeasonGuess] | None:
if user_name is not None: if user_name is not None:
predicate: Callable[[SeasonGuess], bool] = lambda guess: guess.user_name == user_name predicate: Callable[[SeasonGuess], bool] = lambda guess: guess.user.name == user_name
return find_single_or_none(predicate, self.all_season_guesses()) return find_single_or_none_strict(predicate, self.all_season_guesses())
if user_name is None: if user_name is None:
guesses_by: Dict[str, SeasonGuess] = dict() guesses_by: Dict[str, SeasonGuess] = dict()
guess: SeasonGuess guess: SeasonGuess
for guess in self.all_season_guesses(): for guess in self.all_season_guesses():
guesses_by[guess.user_name] = guess guesses_by[guess.user.name] = guess
return guesses_by return guesses_by
@ -184,7 +226,10 @@ class TemplateModel:
Returns a list of all races in the database. Returns a list of all races in the database.
""" """
if self._all_races is None: if self._all_races is None:
self._all_races = db.session.query(Race).order_by(desc(Race.number)).all() 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 return self._all_races
@ -199,32 +244,72 @@ class TemplateModel:
most_recent_result: RaceResult = results[0] most_recent_result: RaceResult = results[0]
predicate: Callable[[Race], bool] = lambda race: race.number == most_recent_result.race.number + 1 predicate: Callable[[Race], bool] = lambda race: race.number == most_recent_result.race.number + 1
return find_first_or_none(predicate, self.all_races()) return find_first_else_none(predicate, self.all_races())
def all_teams(self) -> List[Team]: @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_race_results()[0].race.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_race_results()[0].race.name_sanitized
def all_teams(self, *, include_none: bool) -> List[Team]:
""" """
Returns a list of all teams in the database. Returns a list of all teams in the database.
""" """
if self._all_teams is None: if self._all_teams is None:
self._all_teams = db.session.query(Team).all() self._all_teams = [
Team.from_db_team(db_team)
for db_team in db.session.query(DbTeam).all()
]
return self._all_teams 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 all_drivers(self) -> List[Driver]: def none_team(self) -> Team:
return NONE_TEAM
def all_drivers(self, *, include_none: bool) -> List[Driver]:
""" """
Returns a list of all drivers in the database, including the NONE driver. Returns a list of all drivers in the database.
""" """
if self._all_drivers is None: if self._all_drivers is None:
self._all_drivers = db.session.query(Driver).all() self._all_drivers = [
Driver.from_db_driver(db_driver)
for db_driver in db.session.query(DbDriver).all()
]
return self._all_drivers 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_except_none(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)
Returns a list of all drivers in the database, excluding the NONE driver.
""" def drivers_for_wdc_gained(self) -> List[Driver]:
predicate: Callable[[Driver], bool] = lambda driver: driver.name != "None" predicate: Callable[[Driver], bool] = lambda driver: driver.abbr not in self._wdc_gained_excluded_abbrs
return find_multiple(predicate, self.all_drivers()) return find_multiple_strict(predicate, self.all_drivers(include_none=False))
def none_driver(self) -> Driver:
return NONE_DRIVER
@overload @overload
def drivers_by(self, *, team_name: str) -> List[Driver]: def drivers_by(self, *, team_name: str) -> List[Driver]:
@ -243,16 +328,16 @@ class TemplateModel:
def drivers_by(self, *, team_name: str | None = None) -> 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(predicate, self.all_drivers_except_none(), 2) return find_multiple_strict(predicate, self.all_drivers(include_none=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()
driver: Driver driver: Driver
team: Team team: Team
for team in self.all_teams(): for team in self.all_teams(include_none=False):
drivers_by[team.name] = [] drivers_by[team.name] = []
for driver in self.all_drivers_except_none(): 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

@ -1,15 +1,36 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
{# Simple driver dropdown. Requires list of drivers. #} {# Active user navbar dropdown #}
{% macro driver_select(name='', label='', include_none=true) %} {% macro active_user_dropdown(page) %}
{% if model.all_users() | length > 1 %}
<div class="dropdown">
<button class="btn btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
{{ model.active_user_name_or_everyone() }}
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/{{ page }}/Everyone">Everyone</a></li>
<li>
<hr class="dropdown-divider">
</li>
{% for user in model.all_users() %}
<li><a class="dropdown-item" href="/{{ page }}/{{ user.name_sanitized }}">{{ user.name }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endmacro %}
{# Simple driver select for forms #}
{% macro driver_select(name, label, include_none, drivers=none) %}
<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 }}">
<option value="" selected disabled hidden></option> <option value="" selected disabled hidden></option>
{% if include_none == true %}
{% set drivers = model.all_drivers() %} {% if drivers == none %}
{% else %} {% set drivers = model.all_drivers(include_none=include_none) %}
{% set drivers = model.all_drivers_except_none() %}
{% endif %} {% endif %}
{% for driver in drivers %} {% for driver in drivers %}
@ -20,34 +41,32 @@
</div> </div>
{% endmacro %} {% endmacro %}
{# Driver dropdown where a value might be preselected. Requires list of drivers. #} {# Driver select for forms where a value might be preselected #}
{% macro driver_select_with_preselect(match='', name='', label='', include_none=true) %} {% macro driver_select_with_preselect(driver_match, name, label, include_none, drivers=none) %}
<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 }}">
{# 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 include_none == true %} {% if drivers == none %}
{% set drivers = model.all_drivers() %} {% set drivers = model.all_drivers(include_none=include_none) %}
{% else %}
{% set drivers = model.all_drivers_except_none() %}
{% endif %} {% endif %}
{% for driver in drivers %} {% for driver in drivers %}
{% if match == driver.abbr %} {% if driver_match == driver %}
{% set user_has_chosen.driverpre = "true" %} {% set user_has_chosen.driverpre = true %}
<option selected="selected" value="{{ driver.name }}">{{ driver.abbr }}</option> <option selected="selected" value="{{ driver.name }}">{{ driver.abbr }}</option>
{% else %} {% else %}
<option value="{{ driver.name }}">{{ driver.abbr }}</option> <option value="{{ driver.name }}">{{ driver.abbr }}</option>
{% endif %} {% endif %}
{% if (include_none == true) and (driver.abbr == "None") %} {% if (include_none == true) and (driver == model.none_driver()) %}
<option disabled>──────────</option> <option disabled>──────────</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{# Add an empty default if nothing has been chosen #} {# Add an empty default if nothing has been chosen #}
{% if user_has_chosen.driverpre == "false" %} {% if user_has_chosen.driverpre == false %}
<option value="" selected="selected" disabled="disabled" hidden="hidden"></option> <option value="" selected="selected" disabled="disabled" hidden="hidden"></option>
{% endif %} {% endif %}
</select> </select>
@ -55,12 +74,17 @@
</div> </div>
{% endmacro %} {% endmacro %}
{# Simple team dropdown. Requires list of teams. #} {# Simple team select for forms #}
{% macro team_select(name='', label='') %} {% macro team_select(name, label, include_none, teams=none) %}
<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 }}">
<option value="" selected disabled hidden></option> <option value="" selected disabled hidden></option>
{% for team in model.all_teams() %}
{% if teams == none %}
{% set teams = model.all_teams(include_none=include_none) %}
{% endif %}
{% for team in teams %}
<option value="{{ team.name }}">{{ team.name }}</option> <option value="{{ team.name }}">{{ team.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
@ -68,24 +92,32 @@
</div> </div>
{% endmacro %} {% endmacro %}
{# Team dropdown where a value might be preselected. Requires list of teams. #} {# Team select for forms where a value might be preselected #}
{% macro team_select_with_preselect(match='', name='', label='') %} {% macro team_select_with_preselect(team_match, name, label, include_none, teams=none) %}
<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 }}">
{# 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) %}
{% for team in model.all_teams() %} {% if teams == none %}
{% if match == team.name %} {% set teams = model.all_teams(include_none=include_none) %}
{% set user_has_chosen.teampre = "true" %} {% endif %}
{% for team in teams %}
{% if team_match == team %}
{% set user_has_chosen.teampre = true %}
<option selected="selected" value="{{ team.name }}">{{ team.name }}</option> <option selected="selected" value="{{ team.name }}">{{ team.name }}</option>
{% else %} {% else %}
<option value="{{ team.name }}">{{ team.name }}</option> <option value="{{ team.name }}">{{ team.name }}</option>
{% endif %} {% endif %}
{% if (include_none == true) and (team == model.none_team()) %}
<option disabled>──────────</option>
{% endif %}
{% endfor %} {% endfor %}
{# Add an empty default if nothing has been chosen #} {# Add an empty default if nothing has been chosen #}
{% if user_has_chosen.teampre == "false" %} {% if user_has_chosen.teampre == false %}
<option value="" selected="selected" disabled="disabled" hidden="hidden"></option> <option value="" selected="selected" disabled="disabled" hidden="hidden"></option>
{% endif %} {% endif %}
</select> </select>
@ -94,44 +126,43 @@
{% endmacro %} {% endmacro %}
{# Easy nav-bar entries. When a page sets the active_page variable, the current entry will be underlined #} {# Easy nav-bar entries. When a page sets the active_page variable, the current entry will be underlined #}
{% macro nav_selector(page='', text='') %} {% macro nav_selector(page, text) %}
<a class="nav-link text-nowrap" href="{{ page }}">{% if active_page == page %}<u>{% endif %} {{ text }} <a class="nav-link text-nowrap" href="{{ page }}">{% if active_page == page %}<u>{% endif %} {{ text }}
{# NOTE: This should be set at the top of each template #} {# NOTE: active_page should be set at the top of each template #}
{% if active_page == page %}</u>{% endif %}</a> {% if active_page == page %}</u>{% endif %}</a>
{% endmacro %} {% endmacro %}
{#@formatter:off#} {% macro pxx_guess_colorization(guessed_driver, result) -%}
{% macro pxx_guess_colorization(driver_abbr='', result=none) -%} {% if (guessed_driver == result.offset_from_place_to_guess(-3)) and (guessed_driver != model.none_driver()) %}
{% if (driver_abbr == result.pxx_driver(-3).abbr) and (driver_abbr != "None") %}fw-bold fw-bold
{% elif (driver_abbr == result.pxx_driver(-2).abbr) and (driver_abbr != "None") %}text-danger fw-bold {% elif (guessed_driver == result.offset_from_place_to_guess(-2)) and (guessed_driver != model.none_driver()) %}
{% elif (driver_abbr == result.pxx_driver(-1).abbr) and (driver_abbr != "None") %}text-warning fw-bold text-danger fw-bold
{% elif (driver_abbr == result.pxx_driver(0).abbr) %}text-success fw-bold {% elif (guessed_driver == result.offset_from_place_to_guess(-1)) and (guessed_driver != model.none_driver()) %}
{% elif (driver_abbr == result.pxx_driver(1).abbr) and (driver_abbr != "None") %}text-warning fw-bold text-warning fw-bold
{% elif (driver_abbr == result.pxx_driver(2).abbr) and (driver_abbr != "None") %}text-danger fw-bold {% elif (guessed_driver == result.offset_from_place_to_guess( 0)) %}text-success fw-bold
{% elif (driver_abbr == result.pxx_driver(3).abbr) and (driver_abbr != "None") %}fw-bold{% endif %} {% elif (guessed_driver == result.offset_from_place_to_guess( 1)) and (guessed_driver != model.none_driver()) %}
{% endmacro %} text-warning fw-bold
{% elif (guessed_driver == result.offset_from_place_to_guess( 2)) and (guessed_driver != model.none_driver()) %}
{% macro pxx_points_tooltip_text(driver_abbr='', result=none) -%} text-danger fw-bold
{% if (driver_abbr == result.pxx_driver(-3).abbr) and (driver_abbr != "None") %}1 Point {% elif (guessed_driver == result.offset_from_place_to_guess( 3)) and (guessed_driver != model.none_driver()) %}
{% elif (driver_abbr == result.pxx_driver(-2).abbr) and (driver_abbr != "None") %}3 Points fw-bold
{% elif (driver_abbr == result.pxx_driver(-1).abbr) and (driver_abbr != "None") %}6 Points {% endif %}
{% elif (driver_abbr == result.pxx_driver(0).abbr) %}10 Points
{% elif (driver_abbr == result.pxx_driver(1).abbr) and (driver_abbr != "None") %}6 Points
{% elif (driver_abbr == result.pxx_driver(2).abbr) and (driver_abbr != "None") %}3 Points
{% elif (driver_abbr == result.pxx_driver(3).abbr) and (driver_abbr != "None") %}1 Point
{% else %}0 Points{% endif %}
{%- endmacro %} {%- endmacro %}
{% macro pxx_standing_tooltip_text(result=none) -%} {% macro dnf_guess_colorization(guessed_driver, result) -%}
P{{ result.race.pxx - 3 }}: {{ result.pxx_driver(-3).abbr }} {% if guessed_driver in result.initial_dnf %}text-success fw-bold
P{{ result.race.pxx - 2 }}: {{ result.pxx_driver(-2).abbr }} {% elif (guessed_driver == model.none_driver()) and (result.initial_dnf | length == 0) %}text-success fw-bold
P{{ result.race.pxx - 1 }}: {{ result.pxx_driver(-1).abbr }} {% endif %}
P{{ result.race.pxx }}: {{ result.pxx_driver(0).abbr }} {%- endmacro %}
P{{ result.race.pxx + 1 }}: {{ result.pxx_driver(1).abbr }}
P{{ result.race.pxx + 2 }}: {{ result.pxx_driver(2).abbr }} {# @formatter:off #}
P{{ result.race.pxx + 3 }}: {{ result.pxx_driver(3).abbr }} {% macro pxx_standing_tooltip_text(result) %}
{% endmacro %} {%- for position in range(-3, 4) %}
{#@formatter:on#} {%- set driver = result.offset_from_place_to_guess(position, respect_nc=false) %}
{{- driver.abbr ~ result.driver_standing_position_string(driver) }}
{% endfor %}
{%- endmacro %}
{# @formatter:on #}
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
@ -170,18 +201,18 @@ P{{ result.race.pxx + 3 }}: {{ result.pxx_driver(3).abbr }}
<div class="collapse navbar-collapse" id="navbarCollapse"> <div class="collapse navbar-collapse" id="navbarCollapse">
<div class="navbar-nav me-2"> <div class="navbar-nav me-2">
{{ nav_selector("/race/" ~ (active_user.name_sanitized if active_user is not none else "Everyone"), "Race Picks") }} {{ nav_selector(page="/race/" ~ model.active_user_name_sanitized_or_everyone(), text="Race Picks") }}
{{ nav_selector("/season/" ~ (active_user.name_sanitized if active_user is not none else "Everyone"), "Season Picks") }} {{ nav_selector(page="/season/" ~ model.active_user_name_sanitized_or_everyone(), text="Season Picks") }}
{{ nav_selector("/graphs", "Statistics") }} {{ nav_selector(page="/graphs", text="Statistics") }}
{{ nav_selector("/rules", "Rules") }} {{ nav_selector(page="/rules", text="Rules") }}
</div> </div>
{% block navbar_center %}{% endblock navbar_center %} {% block navbar_center %}{% endblock navbar_center %}
<div class="flex-grow-1"></div> <div class="flex-grow-1"></div>
<div class="navbar-nav"> <div class="navbar-nav">
{{ nav_selector("/result", "Enter Race Result") }} {{ nav_selector(page="/result", text="Enter Race Result") }}
{{ nav_selector("/user", "Manage Users") }} {{ nav_selector(page="/user", text="Manage Users") }}
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,33 +3,25 @@
{% block title %}Formula 10 - Race Result{% endblock title %} {% block title %}Formula 10 - Race Result{% endblock title %}
{% set active_page = "/result" %} {% set active_page = "/result" %}
{% set active_user = none %}
{% block head_extra %} {% block head_extra %}
<link href="../style/draggable.css" rel="stylesheet"> <link href="../static/style/draggable.css" rel="stylesheet">
<script src="../script/draggable.js" defer></script> <script src="../static/script/draggable.js" defer></script>
{% endblock head_extra %} {% endblock head_extra %}
{% set current_race = model.first_race_without_result() %}
{% block navbar_center %} {% block navbar_center %}
{% if model.all_race_results() | length > 0 %} {% if model.all_race_results() | length > 0 %}
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown" <button class="btn btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false"> aria-expanded="false">
{% if active_result is not none %} {{ model.active_result_race_name_or_current_race_name() }}
{{ active_result.race.name }}
{% elif current_race is not none %}
{{ current_race.name }}
{% else %}
{{ model.all_race_results()[0].race.name }}
{% endif %}
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% if model.first_race_without_result() is not none %} {% if model.current_race is not none %}
<li> <li>
<a class="dropdown-item" href="/result/{{ current_race.name }}">{{ current_race.name }}</a> <a class="dropdown-item"
href="/result/{{ model.current_race.name_sanitized }}">{{ model.current_race.name }}</a>
</li> </li>
<li> <li>
<hr class="dropdown-divider"> <hr class="dropdown-divider">
@ -39,7 +31,7 @@
{% for result in model.all_race_results() %} {% for result in model.all_race_results() %}
<li> <li>
<a class="dropdown-item" <a class="dropdown-item"
href="/result/{{ result.race.name }}">{{ result.race.name }}</a> href="/result/{{ result.race.name_sanitized }}">{{ result.race.name }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -56,28 +48,14 @@
<div class="card-body"> <div class="card-body">
<h5 class="card-title"> <h5 class="card-title">
{% if active_result is not none %} {{ model.active_result_race_name_or_current_race_name() }}
{{ active_result.race.name }}
{% elif current_race is not none %}
{{ current_race.name }}
{% else %}
{{ model.all_race_results()[0].race.name }}
{% endif %}
</h5> </h5>
{# @formatter:off #} <form action="/result-enter/{{ model.active_result_race_name_or_current_race_name_sanitized() }}"
<form action="/result-enter/{%- if active_result is not none %}{{ active_result.race.name }}{% else %}{{ current_race.name }}{% endif %}"
method="post"> method="post">
{# @formatter:on #}
<ul id="columns" class="list-group list-group-flush"> <ul id="columns" class="list-group list-group-flush">
{% if active_result is not none %} {% for driver in model.all_drivers_or_active_result_standing_drivers() %}
{% set drivers = active_result.all_positions %}
{% else %}
{% set drivers = model.all_drivers_except_none() %}
{% endif %}
{% for driver in drivers %}
<li class="list-group-item column p-1" draggable="true"> <li class="list-group-item column p-1" draggable="true">
{{ driver.name }} {{ driver.name }}
@ -86,7 +64,7 @@
<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 (active_result is not none) and (driver in active_result.first_dnf_drivers) %}checked{% endif %}> {% if (model.active_result is not none) and (driver in model.active_result.initial_dnf) %}checked{% 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>
@ -95,7 +73,7 @@
<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 (active_result is not none) and (driver in active_result.dnf_drivers) %}checked{% endif %}> {% if (model.active_result is not none) and (driver in model.active_result.all_dnfs) %}checked{% 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>
@ -104,7 +82,7 @@
<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 (active_result is not none) and (driver in active_result.excluded_drivers) %}checked{% endif %}> {% if (model.active_result is not none) and (driver in model.active_result.standing_exclusions) %}checked{% 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>

View File

@ -2,31 +2,10 @@
{% block title %}Formula 10 - Race{% endblock title %} {% block title %}Formula 10 - Race{% endblock title %}
{% set active_page = "/race/" ~ (active_user.name_sanitized if active_user is not none else "Everyone") %} {% set active_page = "/race/" ~ model.active_user_name_sanitized_or_everyone() %}
{% block navbar_center %} {% block navbar_center %}
{% if model.all_users() | length > 1 %} {{ active_user_dropdown(page='race') }}
<div class="dropdown">
<button class="btn btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
{% if active_user is none %}
Everyone
{% else %}
{{ active_user.name }}
{% endif %}
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/race/Everyone">Everyone</a></li>
<li>
<hr class="dropdown-divider">
</li>
{% for user in model.all_users() %}
<li><a class="dropdown-item" href="/race/{{ user.name_sanitized }}">{{ user.name }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock navbar_center %} {% endblock navbar_center %}
{% block body %} {% block body %}
@ -36,7 +15,7 @@
<tr> <tr>
<th scope="col" rowspan="2" class="text-center" style="width: 200px;">Race</th> <th scope="col" rowspan="2" class="text-center" style="width: 200px;">Race</th>
<th scope="col" {% if 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>
@ -50,41 +29,40 @@
<tr> <tr>
<td>&nbsp;</td> <td>&nbsp;</td>
{% if active_user is none %} {# Link should only be visible if all users are visible #}
{% if model.active_user is not none %}
<td class="text-center text-nowrap" style="min-width: 100px;">{{ model.active_user.name }}</td>
{% else %}
{% for user in model.all_users() %} {% for user in model.all_users() %}
<td class="text-center text-nowrap" style="min-width: 100px;"> <td class="text-center text-nowrap" style="min-width: 100px;">
<a href="/race/{{ user.name_sanitized }}" class="link-dark">{{ user.name }}</a> <a href="/race/{{ user.name_sanitized }}" class="link-dark">{{ user.name }}</a>
</td> </td>
{% endfor %} {% endfor %}
{% else %}
<td class="text-center text-nowrap" style="min-width: 100px;">{{ active_user.name }}</td>
{% endif %} {% endif %}
<td>&nbsp;</td> <td>&nbsp;</td>
</tr> </tr>
{% set current_race = model.first_race_without_result() %}
{# Current Result, only displayed for all users overview and if guess is remaining #} {# Current Result, only displayed for all users overview and if guess is remaining #}
{% if (active_user is none) and (model.first_race_without_result() is not none) %} {% if (model.active_user is none) and (model.current_race is not none) %}
<tr class="table-danger"> <tr class="table-danger">
<td class="text-nowrap"> <td class="text-nowrap">
<span class="fw-bold">{{ current_race.number }}:</span> {{ current_race.name }}<br> <span class="fw-bold">{{ model.current_race.number }}:</span> {{ model.current_race.name }}<br>
<small><span class="fw-bold">Guess:</span> P{{ current_race.pxx }}</small> <small><span class="fw-bold">Guess:</span> P{{ model.current_race.place_to_guess }}</small>
</td> </td>
{% for user in model.all_users() %} {% for user in model.all_users() %}
{% set user_guess = model.race_guesses_by(user_name=user.name, race_name=current_race.name) %} {% set user_guess = model.race_guesses_by(user_name=user.name, race_name=model.current_race.name) %}
<td class="text-center text-nowrap"> <td class="text-center text-nowrap">
{% if user_guess is not none %} {% if user_guess is not none %}
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="list-group-item" style="background-color: inherit;"> <li class="list-group-item" style="background-color: inherit;">
{{ user_guess.pxx.abbr }} {{ user_guess.pxx_guess.abbr }}
</li> </li>
<li class="list-group-item" style="background-color: inherit;"> <li class="list-group-item" style="background-color: inherit;">
{{ user_guess.dnf.abbr }} {{ user_guess.dnf_guess.abbr }}
</li> </li>
</ul> </ul>
{% else %} {% else %}
@ -98,28 +76,30 @@
{% endif %} {% endif %}
{# Enter Guess, only displayed for single user focused view and if guess is remaining #} {# Enter Guess, only displayed for single user focused view and if guess is remaining #}
{% if (active_user is not none) and (model.first_race_without_result() is not none) %} {% if (model.active_user is not none) and (model.current_race is not none) %}
<tr class="table-danger"> <tr class="table-danger">
<td class="text-nowrap"> <td class="text-nowrap">
<span class="fw-bold">{{ current_race.number }}:</span> {{ current_race.name }}<br> <span class="fw-bold">{{ model.current_race.number }}:</span> {{ model.current_race.name }}<br>
<small><span class="fw-bold">Guess:</span> P{{ current_race.pxx }}</small> <small><span class="fw-bold">Guess:</span> P{{ model.current_race.place_to_guess }}</small>
</td> </td>
<td> <td>
<form action="/race-guess/{{ current_race.name_sanitized }}/{{ active_user.name_sanitized }}" method="post"> <form action="/race-guess/{{ model.current_race.name_sanitized }}/{{ model.active_user.name_sanitized }}"
{% set user_guess = model.race_guesses_by(user_name=active_user.name, race_name=current_race.name) %} method="post">
{% 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(user_guess.pxx.abbr if user_guess is not none else "", "pxxselect", "P" ~ current_race.pxx ~ ":") }} {{ driver_select_with_preselect(driver_match=user_guess.pxx_guess, name="pxxselect", label="P" ~ model.current_race.place_to_guess ~ ":", include_none=true) }}
<div class="mt-2"></div> <div class="mt-2"></div>
{# Driver DNF Select #} {# Driver DNF Select #}
{{ driver_select_with_preselect(user_guess.dnf.abbr if user_guess is not none else "", "dnfselect", "DNF:") }} {{ driver_select_with_preselect(driver_match=user_guess.dnf_guess, name="dnfselect", label="DNF:", include_none=true) }}
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save"> <input type="submit" class="btn btn-danger mt-2 w-100" value="Save">
</form> </form>
<form action="/race-guess-delete/{{ current_race.name_sanitized }}/{{ active_user.name_sanitized }}" method="post"> <form action="/race-guess-delete/{{ model.current_race.name_sanitized }}/{{ model.active_user.name_sanitized }}"
method="post">
<input type="submit" class="btn btn-dark mt-2 w-100" value="Delete"> <input type="submit" class="btn btn-dark mt-2 w-100" value="Delete">
</form> </form>
</td> </td>
@ -133,29 +113,25 @@
<tr> <tr>
<td class="text-nowrap"> <td class="text-nowrap">
<span class="fw-bold">{{ past_result.race.number }}:</span> {{ past_result.race.name }}<br> <span class="fw-bold">{{ past_result.race.number }}:</span> {{ past_result.race.name }}<br>
<small><span class="fw-bold">Guessed:</span> P{{ past_result.race.pxx }}</small> <small><span class="fw-bold">Guessed:</span> P{{ past_result.race.place_to_guess }}</small>
</td> </td>
{% if active_user is none %} {% for user in model.all_users_or_active_user() %}
{% set users = model.all_users() %}
{% else %}
{% set users = [active_user] %}
{% endif %}
{% for user in users %}
<td class="text-center text-nowrap"> <td class="text-center text-nowrap">
{% set user_guess = model.race_guesses_by(user_name=user.name, race_name=past_result.race.name) %} {% set user_guess = model.race_guesses_by(user_name=user.name, race_name=past_result.race.name) %}
{% if user_guess is not none %} {% if user_guess is not none %}
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="list-group-item {{ pxx_guess_colorization(user_guess.pxx.abbr, past_result) }}"> <li class="list-group-item {{ pxx_guess_colorization(guessed_driver=user_guess.pxx_guess, result=past_result) }}">
<span data-bs-toggle="tooltip" title="{{ pxx_points_tooltip_text(user_guess.pxx.abbr, past_result) }}"> <span data-bs-toggle="tooltip"
{{ user_guess.pxx.abbr }}{% if user_guess.pxx.abbr != "None" %} ({{ past_result.pxx_driver_position_string(user_guess.pxx.name) }}){% endif %} title="{{ past_result.driver_standing_points_string(user_guess.pxx_guess) }}">
{{ user_guess.pxx_guess.abbr ~ past_result.driver_standing_position_string(user_guess.pxx_guess) }}
</span> </span>
</li> </li>
<li class="list-group-item {% if user_guess.dnf.name in past_result.first_dnf_driver_names %}text-success fw-bold{% endif %}"> <li class="list-group-item {{ dnf_guess_colorization(guessed_driver=user_guess.dnf_guess, result=past_result) }}">
<span data-bs-toggle="tooltip" title="{% if user_guess.dnf.name in past_result.first_dnf_driver_names %}10 Points{% else %}0 Points{% endif %}"> <span data-bs-toggle="tooltip"
{{ user_guess.dnf.abbr }} title="{{ past_result.driver_dnf_points_string(user_guess.dnf_guess) }}">
{{ user_guess.dnf_guess.abbr }}
</span> </span>
</li> </li>
</ul> </ul>
@ -165,15 +141,16 @@
</td> </td>
{% endfor %} {% endfor %}
{# Actual result #}
<td class="text-center text-nowrap"> <td class="text-center text-nowrap">
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="list-group-item"> <li class="list-group-item">
<span data-bs-toggle="tooltip" title="{{ pxx_standing_tooltip_text(past_result) }}"> <span data-bs-toggle="tooltip" title="{{ pxx_standing_tooltip_text(result=past_result) }}">
P{{ past_result.race.pxx }}: {{ past_result.pxx_driver().abbr }} P{{ past_result.race.place_to_guess }}: {{ past_result.offset_from_place_to_guess(0).abbr }}
</span> </span>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
DNF: {% for dnf_driver in past_result.first_dnf_drivers %}{{ dnf_driver.abbr }} {% endfor %}</li> DNF: {{ past_result.initial_dnf_string() }}</li>
</ul> </ul>
</td> </td>
</tr> </tr>

View File

@ -2,52 +2,28 @@
{% block title %}Formula 10 - Season{% endblock title %} {% block title %}Formula 10 - Season{% endblock title %}
{% set active_page = "/season/" ~ (active_user.name_sanitized if active_user is not none else "Everyone") %} {% set active_page = "/season/" ~ model.active_user_name_sanitized_or_everyone() %}
{% block navbar_center %} {% block navbar_center %}
{% if model.all_users() | length > 1 %} {{ active_user_dropdown(page='season') }}
<div class="dropdown">
<button class="btn btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
{% if active_user is none %}
Everyone
{% else %}
{{ active_user.name }}
{% endif %}
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/season/Everyone">Everyone</a></li>
<li>
<hr class="dropdown-divider">
</li>
{% for user in model.all_users() %}
<li><a class="dropdown-item" href="/season/{{ user.name }}">{{ user.name }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock navbar_center %} {% endblock navbar_center %}
{% block body %} {% block body %}
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));"> <div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));">
{% if active_user is none %} {% for user in model.all_users_or_active_user() %}
{% set users = model.all_users() %}
{% else %}
{% set users = [active_user] %}
{% endif %}
{% for user in users %}
<div class="card mb-2 shadow-sm" style="width: 450px;"> <div class="card mb-2 shadow-sm" style="width: 450px;">
<div class="card-body"> <div class="card-body">
{% if active_user is none %}
{# Link should only be visible if all users are visible #}
{% if model.active_user is not none %}
<h5 class="card-title">{{ user.name }}</h5>
{% else %}
<a href="/season/{{ user.name }}" class="link-dark"> <a href="/season/{{ user.name }}" class="link-dark">
<h5 class="card-title">{{ user.name }}</h5> <h5 class="card-title">{{ user.name }}</h5>
</a> </a>
{% else %}
<h5 class="card-title">{{ user.name }}</h5>
{% endif %} {% endif %}
{% set user_guess = model.season_guesses_by(user_name=user.name) %} {% set user_guess = model.season_guesses_by(user_name=user.name) %}
@ -58,7 +34,7 @@
<div class="form-floating"> <div class="form-floating">
{% if user_guess is not none %} {% 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: 50px">{{ user_guess.hot_take }}</textarea> style="height: 50px">{{ user_guess.hot_take_string() }}</textarea>
{% else %} {% else %}
<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: 50px"></textarea> style="height: 50px"></textarea>
@ -69,42 +45,40 @@
{# P2 Constructor #} {# P2 Constructor #}
<div class="mt-2"> <div class="mt-2">
{{ team_select_with_preselect(user_guess.p2_team.name if user_guess is not none else "", {{ team_select_with_preselect(team_match=user_guess.p2_wcc, name="p2select", label="P2 in WCC:", include_none=false) }}
"p2select", "P2 in WCC:") }}
</div> </div>
{# Most Overtakes + DNFs #} {# Most Overtakes + DNFs #}
<div class="input-group mt-2"> <div class="input-group mt-2">
{{ driver_select_with_preselect(user_guess.overtake_driver.abbr if user_guess is not none else "", {{ driver_select_with_preselect(driver_match=user_guess.most_overtakes, name="overtakeselect", label="Most overtakes:", include_none=false) }}
"overtakeselect", "Most overtakes:", false) }} {{ driver_select_with_preselect(driver_match=user_guess.most_dnfs, name="dnfselect", label="Most DNFs:", include_none=false) }}
{{ driver_select_with_preselect(user_guess.dnf_driver.abbr if user_guess is not none else "",
"dnfselect", "Most DNFs:", false) }}
</div> </div>
{# Most Gained + Lost #} {# Most Gained + Lost #}
<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?"> <div class="input-group mt-2" data-bs-toggle="tooltip"
{{ driver_select_with_preselect(user_guess.gained_driver.abbr if user_guess is not none else "", title="Which driver will gain/lose the most places in comparison to last season's results?">
"gainedselect", "Most WDC places gained:", false) }} {{ 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(user_guess.lost_driver.abbr if user_guess is not none else "", {{ driver_select_with_preselect(driver_match=user_guess.most_wdc_lost, name="lostselect", label="Most WDC places lost:", include_none=false) }}
"lostselect", "Most WDC places lost:", false) }}
</div> </div>
{# Team-internal Winners #} {# Team-internal Winners #}
<h6 class="card-subtitle mt-2" data-bs-toggle="tooltip" title="Which driver will finish the season higher than his teammate?">Teammate battle winners:</h6> <h6 class="card-subtitle mt-2" data-bs-toggle="tooltip"
title="Which driver will finish the season higher than his teammate?">Teammate battle
winners:</h6>
<div class="grid mt-2" style="width: 450px; row-gap: 0;"> <div class="grid mt-2" style="width: 450px; row-gap: 0;">
{% for team in model.all_teams() %} {% for team in model.all_teams(include_none=false) %}
{% set driver_a_name = model.drivers_by(team_name=team.name)[0].name %} {% set driver_a = model.drivers_by(team_name=team.name)[0] %}
{% set driver_b_name = model.drivers_by(team_name=team.name)[1].name %} {% 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">
<input class="form-check-input" type="radio" <input class="form-check-input" type="radio"
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_name in user_guess.team_winners.teamwinner_driver_names) %}checked="checked"{% endif %}> {% if (user_guess is not none) and (driver_a in user_guess.team_winners) %}checked="checked"{% 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>
</div> </div>
@ -113,31 +87,32 @@
<input class="form-check-input" type="radio" <input class="form-check-input" type="radio"
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_name in user_guess.team_winners.teamwinner_driver_names) %}checked="checked"{% endif %}> {% if (user_guess is not none) and (driver_b in user_guess.team_winners) %}checked="checked"{% 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>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{# Drivers with Podiums #} {# Drivers with Podiums #}
<h6 class="card-subtitle mt-2" data-bs-toggle="tooltip" title="Which driver will reach at least a single podium?">Drivers with podium(s):</h6> <h6 class="card-subtitle mt-2" data-bs-toggle="tooltip"
title="Which driver will reach at least a single podium?">Drivers with podium(s):</h6>
<div class="grid mt-2" style="width: 450px; row-gap: 0;"> <div class="grid mt-2" style="width: 450px; row-gap: 0;">
{% for team in model.all_teams() %} {% for team in model.all_teams(include_none=false) %}
{% set driver_a_name = model.drivers_by(team_name=team.name)[0].name %} {% set driver_a = model.drivers_by(team_name=team.name)[0] %}
{% set driver_b_name = model.drivers_by(team_name=team.name)[1].name %} {% 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">
<input class="form-check-input" type="checkbox" <input class="form-check-input" type="checkbox"
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_name in user_guess.podium_drivers.podium_driver_names) %}checked="checked"{% endif %}> {% if (user_guess is not none) and (driver_a in user_guess.podiums) %}checked="checked"{% 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>
</div> </div>
@ -145,11 +120,11 @@
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" <input class="form-check-input" type="checkbox"
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_name in user_guess.podium_drivers.podium_driver_names) %}checked="checked"{% endif %}> {% if (user_guess is not none) and (driver_b in user_guess.podiums) %}checked="checked"{% 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>
</div> </div>
{% endfor %} {% endfor %}

View File

@ -3,7 +3,6 @@
{% block title %}Formula 10 - Users{% endblock title %} {% block title %}Formula 10 - Users{% endblock title %}
{% set active_page = "/users" %} {% set active_page = "/users" %}
{% set active_user = none %}
{% block body %} {% block body %}
@ -58,8 +57,7 @@
<div class="form-text"> <div class="form-text">
"Deleting" a user just hides it from the user interface without deleting any inputs, your "Deleting" a user just hides it from the user interface without deleting any inputs, your
"pERsoNaL "pERsoNaL DaTa" is not yours anyway.<br>
DaTa" belongs to ME now.<br>
Re-adding a user with the same name will "restore" it. That doesn't mean you're allowed to Re-adding a user with the same name will "restore" it. That doesn't mean you're allowed to
remove everyone though. remove everyone though.
</div> </div>
@ -68,16 +66,16 @@
</div> </div>
{% endif %} {% endif %}
<div class="card mt-2 border-danger shadow-sm"> {# <div class="card mt-2 border-danger shadow-sm">#}
<div class="card-body"> {# <div class="card-body">#}
<h5 class="card-title">Functions that should not be public</h5> {# <h5 class="card-title">Functions that should not be public</h5>#}
<h6 class="card-subtitle mb-2">(F you if you click this without knowing what it does)</h6> {# <h6 class="card-subtitle mb-2">(F you if you click this without knowing what it does)</h6>#}
<a class="btn btn-outline-danger" href="/save/all">Save all data</a> {# <a class="btn btn-outline-danger" href="/save/all">Save all data</a>#}
<a class="btn btn-outline-danger" href="/load/all">Load all data</a> {# <a class="btn btn-outline-danger" href="/load/all">Load all data</a>#}
<a class="btn btn-outline-danger" href="/load/static">Load static data</a> {# <a class="btn btn-outline-danger" href="/load/static">Load static data</a>#}
<a class="btn btn-outline-danger" href="/load/dynamic">Load dynamic data</a> {# <a class="btn btn-outline-danger" href="/load/dynamic">Load dynamic data</a>#}
</div> {# </div>#}
</div> {# </div>#}
{% endblock body %} {% endblock body %}