Compare commits

...

59 Commits

Author SHA1 Message Date
4b3f95764c Disable stuff no longer required
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-09 20:30:35 +01:00
4fc13471f5 Enable docker workflow on all branches
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 15s
2024-03-09 20:05:34 +01:00
8905673d6d Add requests dependency 2024-03-09 20:04:57 +01:00
8b2920f886 Add dummy values to race result
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 20s
These columns are not marked nullable, so ignoring them prevents entering of race results
2024-03-09 19:48:07 +01:00
73273bc5cd Small database migration fixes
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-03 16:02:20 +01:00
d3097038a5 Large database migration
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 15s
2024-03-03 15:38:35 +01:00
96cb8ca891 Fix sorting + standing bug for wcc/wdc
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 23s
2024-03-03 11:24:40 +01:00
1ad558171d Decrease diagram height on large screens
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-03 03:38:25 +01:00
873df8bd8e Only apply horizontal diagram width on mobile
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 15s
2024-03-03 03:34:42 +01:00
b250c47cb3 Update diagrams for landscape mode
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-03 03:31:22 +01:00
6ed5b914e4 Use driver abbr in diagram
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 16s
2024-03-03 03:24:51 +01:00
feb6d27e24 Increase diagram point size
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-03 03:22:56 +01:00
0d598e75a2 Add diagrams to stats page
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 16s
2024-03-03 03:19:51 +01:00
a3d234a754 Add standing diff to stats page
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-03 02:33:21 +01:00
8fcb8c5704 Improve styling consistency
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-03 02:22:34 +01:00
20d177192f Update diagram
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-03 02:15:19 +01:00
27e0231a25 Update diagram
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 15s
2024-03-03 02:10:58 +01:00
325f753d31 Add points history diagram
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 15s
2024-03-03 02:09:06 +01:00
b4794ca42f Add chartjs 2024-03-03 01:01:07 +01:00
481492868b Fix grid layout on PC screen
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-03 00:25:36 +01:00
64d37acc23 Many frontend improvements
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-03 00:06:52 +01:00
c2c71a32e8 Update note
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-02 22:40:03 +01:00
e7d2e960ad Add points to race table
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-02 22:31:40 +01:00
1bd174e73f Add notes to statistics/leaderboard pages 2024-03-02 22:27:25 +01:00
0ea6568082 Add statistics page
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-02 22:18:28 +01:00
09203d9390 Update season guess calculation
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-02 21:49:47 +01:00
5dcebab545 Allow enabling debug endpoints with env var 2024-03-02 21:49:39 +01:00
dfb9360125 Display current season guess state
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-02 20:31:34 +01:00
a0051aacc3 Implement domain version of season_guess_result 2024-03-02 20:31:14 +01:00
d3ee08df36 Add tooltip on leaderboard 2024-03-02 18:03:31 +01:00
0a94222cb2 Color first place(s) red in leaderboard
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 13s
2024-03-02 18:00:56 +01:00
162423ec84 Combine places with equal points in leaderboard
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-02 17:58:06 +01:00
fcc29d2372 Add place to leaderboard
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-02 17:49:04 +01:00
131965e61f Fix date mistakes in static race data
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-02 13:30:24 +01:00
d5d128379d Sort leaderboard table by total points
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-02 13:25:03 +01:00
49939a38b4 Revert race table styling
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-03-02 13:15:20 +01:00
f1090f205b Add initial leaderboards page
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 38s
2024-03-02 13:11:25 +01:00
efcdf5412b Update race table styling 2024-03-02 13:11:16 +01:00
e0e6ec6bd5 Disable more elements when form is closed 2024-03-02 12:48:34 +01:00
abea8aa0c8 Allow disabling timing constraints for development 2024-03-02 12:38:12 +01:00
cf0dc88284 Add initial (untested) race guess points calculation 2024-02-27 21:01:09 +01:00
70fae278a8 Add pytest 2024-02-27 21:00:59 +01:00
262bcc8d5e Add seasonguessresult table 2024-02-27 20:21:37 +01:00
44549f019d Split base model from template + points model 2024-02-27 19:42:21 +01:00
dc9dc3d092 Make race page table columns smaller
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-02-26 23:56:58 +01:00
16be73c1c1 Fix some race page layout issues
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-02-26 23:52:51 +01:00
1f4489b3aa Show place number on race result
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-02-26 23:34:48 +01:00
ab9440cb4a Rename statistics to leaderboard 2024-02-26 23:09:57 +01:00
1e4c9a1ad6 Add race countdown
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 15s
2024-02-26 23:06:34 +01:00
457a80e58e Disable load dynamic admin endpoints
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-02-26 22:35:22 +01:00
005b852836 Fix template syntax error
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-02-26 22:29:44 +01:00
25b945cb62 Clarify DSQ in rules page
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-02-26 22:23:40 +01:00
86cfa5f649 Fix result index bug introduced in previous commit
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-02-26 22:20:21 +01:00
97d67d49ce Date-lock race+season guesses + use errorpage more often
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 17s
2024-02-26 22:15:08 +01:00
2a8c17633e Add error page 2024-02-26 19:51:07 +01:00
3949ad8f04 Add rules page
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 15s
2024-02-26 19:02:32 +01:00
bf9c25e537 Update pxx in static race data 2024-02-26 19:02:22 +01:00
2a0b3bc3f3 Display race time
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s
2024-02-26 17:06:51 +01:00
dbbbd5ccc6 Add times to static race data 2024-02-26 17:06:35 +01:00
57 changed files with 2269 additions and 1029 deletions

View File

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

View File

@ -1,22 +0,0 @@
name,abbr,team_name,country_code
None,None,None,NO
Alexander Albon,ALB,Williams,TH
Fernando Alonso,ALO,Aston Martin,ES
Valteri Bottas,BOT,Sauber,FL
Pierre Gasly,GAS,Alpine,FR
Lewis Hamilton,HAM,Mercedes,UK
Nico Hulkenberg,HUL,Haas,DE
Charles Leclerc,LEC,Ferrari,MC
Kevin Magnussen,MAG,Haas,DK
Lando Norris,NOR,McLaren,UK
Esteban Ocon,OCO,Alpine,FR
Sergio Perez,PER,Red Bull,MX
Oscar Piastri,PIA,McLaren,AU
Daniel Ricciardo,RIC,VCARB,AU
George Russel,RUS,Mercedes,UK
Carlos Sainz,SAI,Ferrari,ES
Logan Sargeant,SAR,Williams,US
Lance Stroll,STR,Aston Martin,CA
Yuki Tsunoda,TSU,VCARB,JP
Max Verstappen,VER,Red Bull,NL
Zhou Guanyu,ZHO,Sauber,CN
1 name abbr team_name country_code
2 None None None NO
3 Alexander Albon ALB Williams TH
4 Fernando Alonso ALO Aston Martin ES
5 Valteri Bottas BOT Sauber FL
6 Pierre Gasly GAS Alpine FR
7 Lewis Hamilton HAM Mercedes UK
8 Nico Hulkenberg HUL Haas DE
9 Charles Leclerc LEC Ferrari MC
10 Kevin Magnussen MAG Haas DK
11 Lando Norris NOR McLaren UK
12 Esteban Ocon OCO Alpine FR
13 Sergio Perez PER Red Bull MX
14 Oscar Piastri PIA McLaren AU
15 Daniel Ricciardo RIC VCARB AU
16 George Russel RUS Mercedes UK
17 Carlos Sainz SAI Ferrari ES
18 Logan Sargeant SAR Williams US
19 Lance Stroll STR Aston Martin CA
20 Yuki Tsunoda TSU VCARB JP
21 Max Verstappen VER Red Bull NL
22 Zhou Guanyu ZHO Sauber CN

View File

@ -1,25 +0,0 @@
name,number,date,pxx
Bahrain,1,2024-03-02,4
Saudi Arabia,2,2024-03-09,17
Australia,3,2024-03-24,5
Japan,4,2024-04-07,11
China,5,2024-04-21,15
Miami,6,2024-05-05,6
Emilia-Romagna,7,2024-05-21,19
Monaco,8,2024-05-26,9
Canada,9,2023-06-09,11
Spain,10,2024-06-23,13
Austria,11,2024-06-30,12
Great Britain,12,2024-07-07,17
Hungary,12,2023-07-23,12
Belgium,13,2024-07-28,13
Netherlands,14,2024-08-25,4
Monza,15,2024-09-01,6
Azerbaijan,16,2024-09-15,10
Singapore,17,2024-09-22,6
Austin,18,2024-10-20,11
Mexico,19,2024-10-27,17
Brazil,20,2024-11-03,14
Las Vegas,21,2024-11-23,8
Qatar,22,2024-12-01,4
Abu Dhabi,23,2024-12-08,5
1 name number date pxx
2 Bahrain 1 2024-03-02 4
3 Saudi Arabia 2 2024-03-09 17
4 Australia 3 2024-03-24 5
5 Japan 4 2024-04-07 11
6 China 5 2024-04-21 15
7 Miami 6 2024-05-05 6
8 Emilia-Romagna 7 2024-05-21 19
9 Monaco 8 2024-05-26 9
10 Canada 9 2023-06-09 11
11 Spain 10 2024-06-23 13
12 Austria 11 2024-06-30 12
13 Great Britain 12 2024-07-07 17
14 Hungary 12 2023-07-23 12
15 Belgium 13 2024-07-28 13
16 Netherlands 14 2024-08-25 4
17 Monza 15 2024-09-01 6
18 Azerbaijan 16 2024-09-15 10
19 Singapore 17 2024-09-22 6
20 Austin 18 2024-10-20 11
21 Mexico 19 2024-10-27 17
22 Brazil 20 2024-11-03 14
23 Las Vegas 21 2024-11-23 8
24 Qatar 22 2024-12-01 4
25 Abu Dhabi 23 2024-12-08 5

View File

@ -1,12 +0,0 @@
name
None
Alpine
Aston Martin
Ferrari
Haas
McLaren
Mercedes
Red Bull
Sauber
VCARB
Williams
1 name
2 None
3 Alpine
4 Aston Martin
5 Ferrari
6 Haas
7 McLaren
8 Mercedes
9 Red Bull
10 Sauber
11 VCARB
12 Williams

View File

@ -23,7 +23,9 @@
flask flask
flask-sqlalchemy flask-sqlalchemy
sqlalchemy sqlalchemy
requests
pytest
]); ]);
in { in {
devShell = pkgs.devshell.mkShell { devShell = pkgs.devshell.mkShell {

View File

@ -1,9 +1,21 @@
import os
from flask import Flask from flask import Flask
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
# Load local ENV variables (can be set when calling the executable)
ENABLE_TIMING: bool = False if os.getenv("DISABLE_TIMING") == "True" else True
print("Running Formula10 with:")
if not ENABLE_TIMING:
print("- Disabled timing constraints")
app: Flask = Flask(__name__) app: Flask = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///formula10.db" app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///formula10.db"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Session cookie is used to propagate message to error page
app.config['SESSION_TYPE'] = 'memcached'
app.config['SECRET_KEY'] = 'ich stinke nach maggi'
app.url_map.strict_slashes = False app.url_map.strict_slashes = False
db: SQLAlchemy = SQLAlchemy() db: SQLAlchemy = SQLAlchemy()
@ -11,22 +23,9 @@ db.init_app(app)
# NOTE: These imports are required to register the routes. They need to be imported after "app" is declared # NOTE: These imports are required to register the routes. They need to be imported after "app" is declared
import formula10.controller.race_controller # type: ignore import formula10.controller.race_controller # type: ignore
import formula10.controller.season_controller # type: ignore import formula10.controller.season_controller
import formula10.controller.admin_controller # type: ignore import formula10.controller.leaderboard_controller
import formula10.controller.statistics_controller
import formula10.controller.rules_controller
# TODO import formula10.controller.admin_controller
# General import formula10.controller.error_controller
# - Choose "place to guess" late before the race? Make a page for this
# - Rules page
# - Make user order changeable using drag'n'drop?
# - Show place when entering race result (would require updating the drag'n'drop code...)
# - Show cards of previous race results, like with season guesses?
# Statistics
# - Auto calculate points
# - Order user table by points + display points somewhere
# - Show current values for some season guesses (e.g. current most dnfs)
# - Generate static diagram using chart.js + templating the js (funny yikes)

View File

@ -1,62 +1,36 @@
from typing import List from flask import render_template, request
from urllib.parse import unquote
from flask import redirect, render_template, request
from werkzeug import Response from werkzeug import Response
from formula10.database.update_queries import update_race_result, update_user from formula10.database.update_queries import update_user
from formula10.database.import_export import export_dynamic_data, reload_dynamic_data, reload_static_data from formula10.domain.template_model import TemplateModel
from formula10.frontend.template_model import TemplateModel
from formula10 import app from formula10 import app
@app.route("/save/all") # @app.route("/result")
def save() -> Response: # def result_root() -> Response:
export_dynamic_data() # return redirect("/result/Current")
return redirect("/")
@app.route("/load/all") # @app.route("/result/<race_name>")
def load() -> Response: # def result_active_race(race_name: str) -> str:
reload_static_data() # race_name = unquote(race_name)
reload_dynamic_data() # model = TemplateModel(active_user_name=None,
return redirect("/") # active_result_race_name=race_name)
# return render_template("result.jinja", model=model)
@app.route("/load/static") # @app.route("/result-enter/<race_name>", methods=["POST"])
def load_static() -> Response: # def result_enter_post(race_name: str) -> Response:
reload_static_data() # race_name = unquote(race_name)
return redirect("/") # pxxs: List[str] = request.form.getlist("pxx-drivers")
# first_dnfs: List[str] = request.form.getlist("first-dnf-drivers")
# dnfs: List[str] = request.form.getlist("dnf-drivers")
# excluded: List[str] = request.form.getlist("excluded-drivers")
# # @todo Ugly
@app.route("/load/dynamic") # race_id: int = Model().race_by(race_name=race_name).id
def load_dynamic() -> Response: # return update_race_result(race_id, pxxs, first_dnfs, dnfs, excluded)
reload_dynamic_data()
return redirect("/")
@app.route("/result")
def result_root() -> Response:
return redirect("/result/Current")
@app.route("/result/<race_name>")
def result_active_race(race_name: str) -> str:
race_name = unquote(race_name)
model = TemplateModel(active_user_name=None,
active_result_race_name=race_name)
return render_template("enter.jinja", model=model)
@app.route("/result-enter/<race_name>", methods=["POST"])
def result_enter_post(race_name: str) -> Response:
race_name = unquote(race_name)
pxxs: List[str] = request.form.getlist("pxx-drivers")
first_dnfs: List[str] = request.form.getlist("first-dnf-drivers")
dnfs: List[str] = request.form.getlist("dnf-drivers")
excluded: List[str] = request.form.getlist("excluded-drivers")
return update_race_result(race_name, pxxs, first_dnfs, dnfs, excluded)
@app.route("/user") @app.route("/user")

View File

@ -0,0 +1,17 @@
from typing import cast
from flask import redirect, render_template, session
from werkzeug import Response
from formula10.domain.template_model import TemplateModel
from formula10 import app
def error_redirect(error_message: str) -> Response:
session["error_message"] = error_message
return redirect(f"/error")
@app.route("/error")
def error_root() -> str:
model = TemplateModel(active_user_name=None, active_result_race_name=None)
message: str = cast(str, session["error_message"])
return render_template("error.jinja", model=model, error_message=message)

View File

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

View File

@ -3,7 +3,9 @@ from flask import redirect, render_template, request
from werkzeug import Response from werkzeug import Response
from formula10.database.update_queries import delete_race_guess, update_race_guess from formula10.database.update_queries import delete_race_guess, update_race_guess
from formula10.frontend.template_model import TemplateModel from formula10.domain.domain_model import Model
from formula10.domain.points_model import PointsModel
from formula10.domain.template_model import TemplateModel
from formula10 import app from formula10 import app
@ -22,8 +24,9 @@ def race_active_user(user_name: str) -> str:
user_name = unquote(user_name) user_name = unquote(user_name)
model = TemplateModel(active_user_name=user_name, model = TemplateModel(active_user_name=user_name,
active_result_race_name=None) active_result_race_name=None)
points = PointsModel()
return render_template("race.jinja", model=model) return render_template("race.jinja", model=model, points=points)
@app.route("/race-guess/<race_name>/<user_name>", methods=["POST"]) @app.route("/race-guess/<race_name>/<user_name>", methods=["POST"])
@ -34,7 +37,11 @@ def race_guess_post(race_name: str, user_name: str) -> Response:
pxx: str | None = request.form.get("pxxselect") pxx: str | None = request.form.get("pxxselect")
dnf: str | None = request.form.get("dnfselect") dnf: str | None = request.form.get("dnfselect")
return update_race_guess(race_name, user_name, pxx, dnf) race_id: int = Model().race_by(race_name=race_name).id
user_id: int = Model().user_by(user_name=user_name).id
return update_race_guess(race_id, user_id,
int(pxx) if pxx is not None else None,
int(dnf) if dnf is not None else None)
@app.route("/race-guess-delete/<race_name>/<user_name>", methods=["POST"]) @app.route("/race-guess-delete/<race_name>/<user_name>", methods=["POST"])
@ -42,4 +49,6 @@ def race_guess_delete_post(race_name: str, user_name: str) -> Response:
race_name = unquote(race_name) race_name = unquote(race_name)
user_name = unquote(user_name) user_name = unquote(user_name)
return delete_race_guess(race_name, user_name) race_id: int = Model().race_by(race_name=race_name).id
user_id: int = Model().user_by(user_name=user_name).id
return delete_race_guess(race_id, user_id)

View File

@ -0,0 +1,10 @@
from flask import render_template
from formula10 import app
from formula10.domain.template_model import TemplateModel
@app.route("/rules")
def rules_root() -> str:
model = TemplateModel(active_user_name=None, active_result_race_name=None)
return render_template("rules.jinja", model=model)

View File

@ -5,8 +5,10 @@ from werkzeug import Response
from formula10.database.model.db_team import DbTeam from formula10.database.model.db_team import DbTeam
from formula10.database.update_queries import update_season_guess from formula10.database.update_queries import update_season_guess
from formula10.frontend.model.team import NONE_TEAM from formula10.domain.domain_model import Model
from formula10.frontend.template_model import TemplateModel from formula10.domain.model.team import NONE_TEAM
from formula10.domain.points_model import PointsModel
from formula10.domain.template_model import TemplateModel
from formula10 import app, db from formula10 import app, db
@ -20,8 +22,9 @@ def season_active_user(user_name: str) -> str:
user_name = unquote(user_name) user_name = unquote(user_name)
model = TemplateModel(active_user_name=user_name, model = TemplateModel(active_user_name=user_name,
active_result_race_name=None) active_result_race_name=None)
points = PointsModel()
return render_template("season.jinja", model=model) return render_template("season.jinja", model=model, points=points)
@app.route("/season-guess/<user_name>", methods=["POST"]) @app.route("/season-guess/<user_name>", methods=["POST"])
@ -37,8 +40,9 @@ 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(DbTeam).all() if team.name != NONE_TEAM.name request.form.get(f"teamwinner-{team.id}") for team in db.session.query(DbTeam).all() if team.id != NONE_TEAM.id
] ]
podium_driver_guesses: List[str] = request.form.getlist("podiumdrivers") podium_driver_guesses: List[str] = request.form.getlist("podiumdrivers")
return update_season_guess(user_name, guesses, team_winner_guesses, podium_driver_guesses) user_id: int = Model().user_by(user_name=user_name).id
return update_season_guess(user_id, guesses, team_winner_guesses, podium_driver_guesses)

View File

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

View File

View File

@ -3,8 +3,8 @@ from formula10.database.model.db_race_result import DbRaceResult
from formula10.database.model.db_user import DbUser from formula10.database.model.db_user import DbUser
from formula10 import db from formula10 import db
def race_has_result(race_name: str) -> bool: def race_has_result(race_id: int) -> bool:
return db.session.query(DbRaceResult).filter_by(race_name=race_name).first() is not None return db.session.query(DbRaceResult).filter_by(race_id=race_id).first() is not None
def user_exists_and_enabled(user_name: str) -> bool: def user_exists_and_enabled(user_name: str) -> bool:
@ -15,9 +15,9 @@ 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 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: def find_single_driver_strict(driver_id: int) -> DbDriver:
db_driver: DbDriver | None = db.session.query(DbDriver).filter_by(name=driver_name).first() db_driver: DbDriver | None = db.session.query(DbDriver).filter_by(id=driver_id).first()
if db_driver is None: if db_driver is None:
raise Exception(f"Could not find driver with name {driver_name} in database") raise Exception(f"Could not find driver with id {driver_id} in database")
return db_driver return db_driver

View File

@ -1,95 +0,0 @@
import csv
import os.path
from typing import List, Any
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]]:
if not os.path.exists(filename):
print(f"Could not load data from file {filename}, as it doesn't exist!")
return []
with open(filename, "r", newline="") as file:
reader = csv.reader(file, delimiter=",")
next(reader, None) # skip header
return list(reader)
def write_csv(filename: str, objects: List[Any]):
if len(objects) == 0:
print(f"Could not write objects to file {filename}, as no objects were given!")
return
with open(filename, "w", newline="") as file:
writer = csv.writer(file, delimiter=",")
writer.writerow(objects[0].__csv_header__)
for obj in objects:
writer.writerow(obj.to_csv())
# Reload static database data, this has to be called from the app context
def reload_static_data():
print("Initializing Database with Static Values...")
# Create it (if it doesn't exist!)
db.create_all()
# Clear static data
db.session.query(DbTeam).delete()
db.session.query(DbDriver).delete()
db.session.query(DbRace).delete()
# Reload static data
for row in load_csv("data/static_import/teams.csv"):
db.session.add(DbTeam.from_csv(row))
for row in load_csv("data/static_import/drivers.csv"):
db.session.add(DbDriver.from_csv(row))
for row in load_csv("data/static_import/races.csv"):
db.session.add(DbRace.from_csv(row))
db.session.commit()
def reload_dynamic_data():
print("Initializing Database with Dynamic Values...")
# Create it (if it doesn't exist!)
db.create_all()
# Clear dynamic data
db.session.query(DbUser).delete()
db.session.query(DbRaceResult).delete()
db.session.query(DbRaceGuess).delete()
db.session.query(DbSeasonGuess).delete()
# Reload dynamic data
for row in load_csv("data/dynamic_export/users.csv"):
db.session.add(DbUser.from_csv(row))
for row in load_csv("data/dynamic_export/raceresults.csv"):
db.session.add(DbRaceResult.from_csv(row))
for row in load_csv("data/dynamic_export/raceguesses.csv"):
db.session.add(DbRaceGuess.from_csv(row))
for row in load_csv("data/dynamic_export/seasonguesses.csv"):
db.session.add(DbSeasonGuess.from_csv(row))
db.session.commit()
def export_dynamic_data():
print("Exporting Userdata...")
users: List[DbUser] = db.session.query(DbUser).all()
raceresults: List[DbRaceResult] = db.session.query(DbRaceResult).all()
raceguesses: List[DbRaceGuess] = db.session.query(DbRaceGuess).all()
seasonguesses: List[DbSeasonGuess] = db.session.query(DbSeasonGuess).all()
write_csv("data/dynamic_export/users.csv", users)
write_csv("data/dynamic_export/raceresults.csv", raceresults)
write_csv("data/dynamic_export/raceguesses.csv", raceguesses)
write_csv("data/dynamic_export/seasonguesses.csv", seasonguesses)

View File

View File

@ -1,5 +1,4 @@
from typing import List from sqlalchemy import Integer, 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.db_team import DbTeam from formula10.database.model.db_team import DbTeam
@ -13,21 +12,14 @@ class DbDriver(db.Model):
""" """
__tablename__ = "driver" __tablename__ = "driver"
def __init__(self, *, name: str): def __init__(self, *, id: int):
self.name = name # Primary key self.id = id # Primary key
@classmethod id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=False)
def from_csv(cls, row: List[str]): name: Mapped[str] = mapped_column(String(32), nullable=False, unique=True)
db_driver: DbDriver = cls(name=str(row[0])) abbr: Mapped[str] = mapped_column(String(4), nullable=False, unique=True)
db_driver.abbr = str(row[1]) team_id: Mapped[str] = mapped_column(ForeignKey("team.id"), nullable=False)
db_driver.team_name = str(row[2]) country_code: Mapped[str] = mapped_column(String(2), nullable=False) # alpha-2 code
db_driver.country_code = str(row[3])
return db_driver
name: Mapped[str] = mapped_column(String(32), primary_key=True)
abbr: Mapped[str] = mapped_column(String(4))
team_name: Mapped[str] = mapped_column(ForeignKey("team.name"))
country_code: Mapped[str] = mapped_column(String(2)) # alpha-2 code
# Relationships # Relationships
team: Mapped[DbTeam] = relationship("DbTeam", foreign_keys=[team_name]) team: Mapped[DbTeam] = relationship("DbTeam", foreign_keys=[team_id])

View File

@ -1,5 +1,4 @@
from datetime import datetime from datetime import datetime
from typing import List
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
@ -13,22 +12,11 @@ class DbRace(db.Model):
""" """
__tablename__ = "race" __tablename__ = "race"
def __init__(self, *, name: str, number: int, date: datetime, pxx: int): def __init__(self, *, id: int):
self.name = name # Primary key self.id = id # Primary key
self.number = number id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=False)
self.date = date name: Mapped[str] = mapped_column(String(64), nullable=False, unique=True)
self.pxx = pxx number: Mapped[int] = mapped_column(Integer, nullable=False, unique=True)
date: Mapped[datetime] = mapped_column(DateTime, nullable=False, unique=True)
@classmethod pxx: Mapped[int] = mapped_column(Integer, nullable=False) # This is the place to guess
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)
number: Mapped[int] = mapped_column(Integer)
date: Mapped[datetime] = mapped_column(DateTime)
pxx: Mapped[int] = mapped_column(Integer) # This is the place to guess

View File

@ -1,4 +1,3 @@
from typing import Any, List
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@ -14,38 +13,18 @@ class DbRaceGuess(db.Model):
It stores the corresponding race and the guessed drivers for PXX and DNF. It stores the corresponding race and the guessed drivers for PXX and DNF.
""" """
__tablename__ = "raceguess" __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): def __init__(self, *, user_id: int, race_id: int):
self.user_name = user_name # Primary key self.user_id = user_id # Primary key
self.race_name = race_name # Primary key self.race_id = race_id # Primary key
self.dnf_driver_name = dnf_driver_name user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), primary_key=True)
self.pxx_driver_name = pxx_driver_name race_id: Mapped[int] = mapped_column(ForeignKey("race.id"), primary_key=True)
pxx_driver_id: Mapped[int] = mapped_column(ForeignKey("driver.id"), nullable=False)
@classmethod dnf_driver_id: Mapped[int] = mapped_column(ForeignKey("driver.id"), nullable=False)
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 # Relationships
user: Mapped[DbUser] = relationship("DbUser", foreign_keys=[user_name]) user: Mapped[DbUser] = relationship("DbUser", foreign_keys=[user_id])
race: Mapped[DbRace] = relationship("DbRace", foreign_keys=[race_name]) race: Mapped[DbRace] = relationship("DbRace", foreign_keys=[race_id])
pxx: Mapped[DbDriver] = relationship("DbDriver", foreign_keys=[pxx_driver_name]) pxx: Mapped[DbDriver] = relationship("DbDriver", foreign_keys=[pxx_driver_id])
dnf: Mapped[DbDriver] = relationship("DbDriver", foreign_keys=[dnf_driver_name]) dnf: Mapped[DbDriver] = relationship("DbDriver", foreign_keys=[dnf_driver_id])

View File

@ -1,6 +1,6 @@
from typing import Any, List
from sqlalchemy import ForeignKey, String from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from formula10.database.model.db_driver import DbDriver
from formula10.database.model.db_race import DbRace from formula10.database.model.db_race import DbRace
from formula10 import db from formula10 import db
@ -11,39 +11,20 @@ class DbRaceResult(db.Model):
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. 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" __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): def __init__(self, *, race_id: int):
self.race_name = race_name # Primary key self.race_id = race_id # Primary key
self.pxx_driver_names_json = pxx_driver_names_json race_id: Mapped[int] = mapped_column(ForeignKey("race.id"), primary_key=True)
self.first_dnf_driver_names_json = first_dnf_driver_names_json pxx_driver_ids_json: Mapped[str] = mapped_column(String(1024), nullable=False)
self.dnf_driver_names_json = dnf_driver_names_json first_dnf_driver_ids_json: Mapped[str] = mapped_column(String(1024), nullable=False)
self.excluded_driver_names_json = excluded_driver_names_json dnf_driver_ids_json: Mapped[str] = mapped_column(String(1024), nullable=False)
excluded_driver_ids_json: Mapped[str] = mapped_column(String(1024), nullable=False)
@classmethod fastest_lap_id: Mapped[int] = mapped_column(ForeignKey("driver.id"), nullable=False)
def from_csv(cls, row: List[str]): sprint_dnf_driver_ids_json: Mapped[str] = mapped_column(String(1024), nullable=False)
db_race_result: DbRaceResult = cls(race_name=str(row[0]), sprint_points_json: Mapped[str] = mapped_column(String(1024), nullable=False)
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 # Relationships
race: Mapped[DbRace] = relationship("DbRace", foreign_keys=[race_name]) race: Mapped[DbRace] = relationship("DbRace", foreign_keys=[race_id])
fastest_lap_driver: Mapped[DbDriver] = relationship("DbDriver", foreign_keys=[fastest_lap_id])

View File

@ -1,4 +1,3 @@
from typing import Any, List
from sqlalchemy import ForeignKey, String from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@ -12,56 +11,24 @@ class DbSeasonGuess(db.Model):
A collection of bonus guesses for the entire season. A collection of bonus guesses for the entire season.
""" """
__tablename__ = "seasonguess" __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): def __init__(self, *, user_id: int):
self.user_name = user_name # Primary key self.user_id = user_id # Primary key
self.team_winners_driver_names_json = team_winners_driver_names_json user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), primary_key=True)
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) hot_take: Mapped[str | None] = mapped_column(String(512), nullable=True)
p2_team_name: Mapped[str | None] = mapped_column(ForeignKey("team.name"), nullable=True) p2_team_id: Mapped[int | None] = mapped_column(ForeignKey("team.id"), nullable=True)
overtake_driver_name: Mapped[str | None] = mapped_column(ForeignKey("driver.name"), nullable=True) overtake_driver_id: Mapped[int | None] = mapped_column(ForeignKey("driver.id"), nullable=True)
dnf_driver_name: Mapped[str | None] = mapped_column(ForeignKey("driver.name"), nullable=True) dnf_driver_id: Mapped[int | None] = mapped_column(ForeignKey("driver.id"), nullable=True)
gained_driver_name: Mapped[str | None] = mapped_column(ForeignKey("driver.name"), nullable=True) gained_driver_id: Mapped[int | None] = mapped_column(ForeignKey("driver.id"), nullable=True)
lost_driver_name: Mapped[str | None] = mapped_column(ForeignKey("driver.name"), nullable=True) lost_driver_id: Mapped[int | None] = mapped_column(ForeignKey("driver.id"), nullable=True)
team_winners_driver_names_json: Mapped[str] = mapped_column(String(1024)) team_winners_driver_ids_json: Mapped[str] = mapped_column(String(1024))
podium_drivers_driver_names_json: Mapped[str] = mapped_column(String(1024)) podium_drivers_driver_ids_json: Mapped[str] = mapped_column(String(1024))
# Relationships # Relationships
user: Mapped[DbUser] = relationship("DbUser", foreign_keys=[user_name]) user: Mapped[DbUser] = relationship("DbUser", foreign_keys=[user_id])
p2_team: Mapped[DbTeam | None] = relationship("DbTeam", foreign_keys=[p2_team_name]) p2_team: Mapped[DbTeam | None] = relationship("DbTeam", foreign_keys=[p2_team_id])
overtake_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[overtake_driver_name]) overtake_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[overtake_driver_id])
dnf_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[dnf_driver_name]) dnf_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[dnf_driver_id])
gained_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[gained_driver_name]) gained_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[gained_driver_id])
lost_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[lost_driver_name]) lost_driver: Mapped[DbDriver | None] = relationship("DbDriver", foreign_keys=[lost_driver_id])

View File

@ -0,0 +1,22 @@
from sqlalchemy import Boolean, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from formula10 import db
from formula10.database.model.db_user import DbUser
class DbSeasonGuessResult(db.Model):
"""
Manually entered results for the season bonus guesses.
"""
__tablename__ = "seasonguessresult"
def __init__(self, *, user_id: int):
self.user_id = user_id # Primary key
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), primary_key=True)
hot_take_correct: Mapped[bool] = mapped_column(Boolean, nullable=False)
overtakes_correct: Mapped[bool] = mapped_column(Boolean, nullable=False)
# Relationships
user: Mapped[DbUser] = relationship("DbUser", foreign_keys=[user_id])

View File

@ -1,5 +1,4 @@
from typing import List from sqlalchemy import Integer, String
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from formula10 import db from formula10 import db
@ -11,12 +10,8 @@ class DbTeam(db.Model):
""" """
__tablename__ = "team" __tablename__ = "team"
def __init__(self, *, name: str): def __init__(self, *, id: int):
self.name = name # Primary key self.id = id # Primary key
@classmethod id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=False)
def from_csv(cls, row: List[str]): name: Mapped[str] = mapped_column(String(32), nullable=False, unique=True)
db_team: DbTeam = cls(name=str(row[0]))
return db_team
name: Mapped[str] = mapped_column(String(32), primary_key=True)

View File

@ -1,5 +1,4 @@
from typing import Any, List from sqlalchemy import Boolean, Integer, String
from sqlalchemy import Boolean, String
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from formula10 import db from formula10 import db
@ -10,23 +9,11 @@ class DbUser(db.Model):
A user that can guess races (name only). A user that can guess races (name only).
""" """
__tablename__ = "user" __tablename__ = "user"
__csv_header__ = ["name", "enabled"]
def __init__(self, *, name: str, enabled: bool): def __init__(self, *, id: int | None):
self.name = name # Primary key if id is not None:
self.id = id # Primary key
self.enabled = enabled id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(32), nullable=False, unique=True)
@classmethod enabled: Mapped[bool] = mapped_column(Boolean, nullable=False)
def from_csv(cls, row: List[str]):
db_user: DbUser = cls(name=str(row[0]), enabled=True if str(row[1])=="True" else False)
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

View File

@ -1,176 +1,206 @@
import json import json
from typing import Dict, List, cast from typing import List, cast
from urllib.parse import quote
from flask import redirect from flask import redirect
from werkzeug import Response from werkzeug import Response
from formula10.controller.error_controller import error_redirect
from formula10.database.common_queries import race_has_result, user_exists_and_disabled, user_exists_and_enabled from formula10.database.common_queries import race_has_result, user_exists_and_disabled, user_exists_and_enabled
from formula10.database.model.db_race_guess import DbRaceGuess from formula10.database.model.db_race_guess import DbRaceGuess
from formula10.database.model.db_race_result import DbRaceResult
from formula10.database.model.db_season_guess import DbSeasonGuess from formula10.database.model.db_season_guess import DbSeasonGuess
from formula10.database.model.db_user import DbUser from formula10.database.model.db_user import DbUser
from formula10.database.validation import any_is_none, positions_are_contiguous from formula10.database.validation import any_is_none, race_has_started
from formula10 import db from formula10 import ENABLE_TIMING, db
def find_or_create_race_guess(user_name: str, race_name: str) -> DbRaceGuess: def find_or_create_race_guess(user_id: int, race_id: int) -> 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: DbRaceGuess | None = db.session.query(DbRaceGuess).filter_by(user_name=user_name, race_name=race_name).first() race_guess: DbRaceGuess | None = db.session.query(DbRaceGuess).filter_by(user_id=user_id, race_id=race_id).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 = DbRaceGuess(user_name=user_name, race_name=race_name, pxx_driver_name="TEMP", dnf_driver_name="TEMP") race_guess = DbRaceGuess(user_id=user_id, race_id=race_id)
race_guess.pxx_driver_id = 9999
race_guess.dnf_driver_id = 9999
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(DbRaceGuess).filter_by(user_name=user_name, race_name=race_name).first() race_guess = db.session.query(DbRaceGuess).filter_by(user_id=user_id, race_id=race_id).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")
return race_guess return race_guess
def update_race_guess(race_name: str, user_name: str, pxx_select: str | None, dnf_select: str | None) -> Response: def update_race_guess(race_id: int, user_id: int, pxx_select_id: int | None, dnf_select_id: int | None) -> Response:
if any_is_none(pxx_select, dnf_select): if any_is_none(pxx_select_id, dnf_select_id):
return redirect(f"/race/{quote(user_name)}") return error_redirect(f"Picks for race \"{race_id}\" were not saved, because you did not fill all the fields.")
pxx_driver_name: str = cast(str, pxx_select) if ENABLE_TIMING and race_has_started(race_id=race_id):
dnf_driver_name: str = cast(str, dnf_select) return error_redirect(f"No picks for race \"{race_id}\" can be entered, as this race has already started.")
# TODO: Date-lock this. Otherwise there is a period of time after the race if race_has_result(race_id):
# but before the result where guesses can still be entered return error_redirect(f"No picks for race \"{race_id}\" can be entered, as this race has already finished.")
# We can't guess for races that are already over
if race_has_result(race_name):
return redirect(f"/race/{quote(user_name)}")
race_guess: DbRaceGuess = find_or_create_race_guess(user_name, race_name) pxx_driver_id: int = cast(int, pxx_select_id)
race_guess.pxx_driver_name = pxx_driver_name dnf_driver_id: int = cast(int, dnf_select_id)
race_guess.dnf_driver_name = dnf_driver_name
race_guess: DbRaceGuess = find_or_create_race_guess(user_id, race_id)
race_guess.pxx_driver_id = pxx_driver_id
race_guess.dnf_driver_id = dnf_driver_id
db.session.commit() db.session.commit()
return redirect("/race/Everyone") return redirect("/race/Everyone")
def delete_race_guess(race_name: str, user_name: str) -> Response: def delete_race_guess(race_id: int, user_id: int) -> Response:
# Don't change guesses that are already over # Don't change guesses that are already over
if race_has_result(race_name): if ENABLE_TIMING and race_has_started(race_id=race_id):
return redirect(f"/race/{quote(user_name)}") return error_redirect(f"No picks for race with id \"{race_id}\" can be deleted, as this race has already started.")
db.session.query(DbRaceGuess).filter_by(race_name=race_name, user_name=user_name).delete() if race_has_result(race_id):
return error_redirect(f"No picks for race \"{race_id}\" can be deleted, as this race has already finished.")
# Does not throw if row doesn't exist
db.session.query(DbRaceGuess).filter_by(race_id=race_id, user_id=user_id).delete()
db.session.commit() db.session.commit()
return redirect("/race/Everyone") return redirect("/race/Everyone")
def find_or_create_season_guess(user_name: str) -> DbSeasonGuess: def find_or_create_season_guess(user_id: int) -> DbSeasonGuess:
# 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: DbSeasonGuess | None = db.session.query(DbSeasonGuess).filter_by(user_name=user_name).first() season_guess: DbSeasonGuess | None = db.session.query(DbSeasonGuess).filter_by(user_id=user_id).first()
if season_guess is not None: if season_guess is not None:
return season_guess return season_guess
# Insert a new SeasonGuess # Insert a new SeasonGuess
season_guess = DbSeasonGuess(user_name=user_name, team_winners_driver_names_json=json.dumps(["TEMP"]), podium_drivers_driver_names_json=json.dumps(["TEMP"])) season_guess = DbSeasonGuess(user_id=user_id)
season_guess.team_winners_driver_ids_json=json.dumps(["9999"])
season_guess.podium_drivers_driver_ids_json=json.dumps(["9999"])
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(DbSeasonGuess).filter_by(user_name=user_name).first() season_guess = db.session.query(DbSeasonGuess).filter_by(user_id=user_id).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")
return season_guess return season_guess
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_id: int, 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: DbSeasonGuess = find_or_create_season_guess(user_name) if ENABLE_TIMING and race_has_started(race_id=1):
return error_redirect("No season picks can be entered, as the season has already begun!")
season_guess: DbSeasonGuess = find_or_create_season_guess(user_id)
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_id = guesses[1] # type: ignore
season_guess.overtake_driver_name = guesses[2] # type: ignore season_guess.overtake_driver_id = guesses[2] # type: ignore
season_guess.dnf_driver_name = guesses[3] # type: ignore season_guess.dnf_driver_id = guesses[3] # type: ignore
season_guess.gained_driver_name = guesses[4] # type: ignore season_guess.gained_driver_id = guesses[4] # type: ignore
season_guess.lost_driver_name = guesses[5] # type: ignore season_guess.lost_driver_id = guesses[5] # type: ignore
season_guess.team_winners_driver_names_json = json.dumps(team_winner_guesses) season_guess.team_winners_driver_ids_json = json.dumps(team_winner_guesses)
season_guess.podium_drivers_driver_names_json = json.dumps(podium_driver_guesses) season_guess.podium_drivers_driver_ids_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) -> DbRaceResult: # def find_or_create_race_result(race_id: int) -> DbRaceResult:
# There can be a single RaceResult at most, since race_name is the primary key # # There can be a single RaceResult at most, since race_name is the primary key
race_result: DbRaceResult | None = db.session.query(DbRaceResult).filter_by(race_name=race_name).first() # race_result: DbRaceResult | None = db.session.query(DbRaceResult).filter_by(race_id=race_id).first()
if race_result is not None: # if race_result is not None:
return race_result # return race_result
race_result = DbRaceResult(race_name=race_name, # race_result = DbRaceResult(race_id=race_id)
pxx_driver_names_json=json.dumps(["TEMP"]), # race_result.pxx_driver_ids_json = json.dumps(["9999"])
first_dnf_driver_names_json=json.dumps(["TEMP"]), # race_result.first_dnf_driver_ids_json = json.dumps(["9999"])
dnf_driver_names_json=json.dumps(["TEMP"]), # race_result.dnf_driver_ids_json = json.dumps(["9999"])
excluded_driver_names_json=json.dumps(["TEMP"])) # race_result.excluded_driver_ids_json = json.dumps(["9999"])
db.session.add(race_result)
db.session.commit()
# Double check if database insertion worked and obtain any values set by the database # race_result.fastest_lap_id = 9999
race_result = db.session.query(DbRaceResult).filter_by(race_name=race_name).first() # race_result.sprint_dnf_driver_ids_json = json.dumps(["9999"])
if race_result is None: # race_result.sprint_points_json = json.dumps({"9999": "9999"})
raise Exception("Failed adding RaceResult to the database")
return race_result # db.session.add(race_result)
# db.session.commit()
# # Double check if database insertion worked and obtain any values set by the database
# race_result = db.session.query(DbRaceResult).filter_by(race_id=race_id).first()
# if race_result is None:
# raise Exception("Failed adding RaceResult to the database")
# return race_result
def update_race_result(race_name: str, pxx_driver_names_list: List[str], first_dnf_driver_names_list: List[str], dnf_driver_names_list: List[str], excluded_driver_names_list: List[str]) -> Response: # def update_race_result(race_id: int, pxx_driver_ids_list: List[str], first_dnf_driver_ids_list: List[str], dnf_driver_ids_list: List[str], excluded_driver_ids_list: List[str]) -> Response:
# Use strings as keys, as these dicts will be serialized to json # if ENABLE_TIMING and not race_has_started(race_id=race_id):
pxx_driver_names: Dict[str, str] = { # return error_redirect("No race result can be entered, as the race has not begun!")
str(position + 1): driver for position, driver in enumerate(pxx_driver_names_list)
}
# Not counted drivers have to be at the end # # Use strings as keys, as these dicts will be serialized to json
excluded_driver_names: Dict[str, str] = { # pxx_driver_names: Dict[str, str] = {
str(position + 1): driver for position, driver in enumerate(pxx_driver_names_list) # str(position + 1): driver_id for position, driver_id in enumerate(pxx_driver_ids_list)
if driver in excluded_driver_names_list # }
}
if len(excluded_driver_names) > 0 and (not "20" in excluded_driver_names or not positions_are_contiguous(list(excluded_driver_names.keys()))):
return redirect(f"/result/{quote(race_name)}")
# First DNF drivers have to be contained in DNF drivers # # Not counted drivers have to be at the end
for driver_name in first_dnf_driver_names_list: # excluded_driver_names: Dict[str, str] = {
if driver_name not in dnf_driver_names_list: # str(position + 1): driver_id for position, driver_id in enumerate(pxx_driver_ids_list)
dnf_driver_names_list.append(driver_name) # if driver_id in excluded_driver_ids_list
# }
# if len(excluded_driver_names) > 0 and (not "20" in excluded_driver_names or not positions_are_contiguous(list(excluded_driver_names.keys()))):
# return error_redirect("Race result was not saved, as excluded drivers must be contiguous and at the end of the field!")
# There can't be dnfs but no initial dnfs # # First DNF drivers have to be contained in DNF drivers
if len(dnf_driver_names_list) > 0 and len(first_dnf_driver_names_list) == 0: # for driver_id in first_dnf_driver_ids_list:
return redirect(f"/result/{quote(race_name)}") # if driver_id not in dnf_driver_ids_list:
# dnf_driver_ids_list.append(driver_id)
race_result: DbRaceResult = find_or_create_race_result(race_name) # # There can't be dnfs but no initial dnfs
race_result.pxx_driver_names_json = json.dumps(pxx_driver_names) # if len(dnf_driver_ids_list) > 0 and len(first_dnf_driver_ids_list) == 0:
race_result.first_dnf_driver_names_json = json.dumps(first_dnf_driver_names_list) # return error_redirect("Race result was not saved, as there cannot be DNFs without (an) initial DNF(s)!")
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() # race_result: DbRaceResult = find_or_create_race_result(race_id)
# race_result.pxx_driver_ids_json = json.dumps(pxx_driver_names)
# race_result.first_dnf_driver_ids_json = json.dumps(first_dnf_driver_ids_list)
# race_result.dnf_driver_ids_json = json.dumps(dnf_driver_ids_list)
# race_result.excluded_driver_ids_json = json.dumps(excluded_driver_ids_list)
return redirect(f"/result/{quote(race_name)}") # # @todo Dummy values
# race_result.fastest_lap_id = NONE_DRIVER.id
# race_result.sprint_dnf_driver_ids_json = json.dumps([NONE_DRIVER.id])
# race_result.sprint_points_json = json.dumps({NONE_DRIVER.id: 0})
# db.session.commit()
# race: DbRace | None = db.session.query(DbRace).filter_by(id=race_id).first()
# if race is None:
# raise Exception(f"Could not find DbRace with id {race_id}")
# return redirect(f"/result/{quote(race.name)}")
def update_user(user_name: str | None, add: bool = False, delete: bool = False) -> Response: def update_user(user_name: str | None, add: bool = False, delete: bool = False) -> Response:
if user_name is None or len(user_name) < 3: if user_name is None:
return redirect("/user") return error_redirect("Invalid request: Cannot add/delete user because it is \"None\"!")
if not add and not delete: if not add and not delete:
return redirect("/user") return error_redirect("Invalid request: Can either add or delete user!")
if add and delete: if add and delete:
return redirect("/user") return error_redirect("Invalid request: Can either add or delete user!")
if add: if add:
if len(user_name) < 3:
return error_redirect(f"User \"{user_name}\" was not added, because the username must contain at least 3 characters!")
if user_exists_and_enabled(user_name): if user_exists_and_enabled(user_name):
return redirect("/user") return error_redirect(f"User \"{user_name}\" was not added, because it already exists!")
elif user_exists_and_disabled(user_name): elif user_exists_and_disabled(user_name):
disabled_user: DbUser | None = db.session.query(DbUser).filter_by(name=user_name, enabled=False).first() disabled_user: DbUser | None = db.session.query(DbUser).filter_by(name=user_name, enabled=False).first()
@ -180,7 +210,9 @@ def update_user(user_name: str | None, add: bool = False, delete: bool = False)
disabled_user.enabled = True disabled_user.enabled = True
else: else:
user: DbUser = DbUser(name=user_name, enabled=True) user: DbUser = DbUser(id=None)
user.name = user_name
user.enabled = True
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
@ -188,7 +220,10 @@ def update_user(user_name: str | None, add: bool = False, delete: bool = False)
return redirect("/user") return redirect("/user")
if delete: if delete:
if user_exists_and_enabled(user_name): if user_exists_and_disabled(user_name):
return error_redirect(f"User \"{user_name}\" was not deleted, because it does not exist!")
elif user_exists_and_enabled(user_name):
enabled_user: DbUser | None = db.session.query(DbUser).filter_by(name=user_name, enabled=True).first() enabled_user: DbUser | None = db.session.query(DbUser).filter_by(name=user_name, enabled=True).first()
if enabled_user is None: if enabled_user is None:
raise Exception("update_user couldn't disable user") raise Exception("update_user couldn't disable user")
@ -196,6 +231,9 @@ def update_user(user_name: str | None, add: bool = False, delete: bool = False)
enabled_user.enabled = False enabled_user.enabled = False
db.session.commit() db.session.commit()
else:
return error_redirect(f"User \"{user_name}\" was not deleted, because it does not exist!")
return redirect("/user") return redirect("/user")
raise Exception("update_user received illegal combination of arguments") raise Exception("update_user received illegal combination of arguments")

View File

@ -1,4 +1,9 @@
from typing import Any, Callable, Iterable, List, TypeVar from datetime import datetime
from typing import Any, Callable, Iterable, List, TypeVar, overload
from formula10.database.model.db_race import DbRace
from formula10 import db
from formula10.domain.model.race import Race
_T = TypeVar("_T") _T = TypeVar("_T")
@ -21,6 +26,28 @@ def positions_are_contiguous(positions: List[str]) -> bool:
# [2, 3, 4, 5]: 2 + 3 == 5 # [2, 3, 4, 5]: 2 + 3 == 5
return positions_sorted[0] + len(positions_sorted) - 1 == positions_sorted[-1] return positions_sorted[0] + len(positions_sorted) - 1 == positions_sorted[-1]
@overload
def race_has_started(*, race: Race) -> bool:
return race_has_started(race=race)
@overload
def race_has_started(*, race_id: int) -> bool:
return race_has_started(race_id=race_id)
def race_has_started(*, race: Race | None = None, race_id: int | None = None) -> bool:
if race is None and race_id is not None:
_race: DbRace | None = db.session.query(DbRace).filter_by(id=race_id).first()
if _race is None:
raise Exception(f"Couldn't obtain race with id {race_id} to check date")
return datetime.now() > _race.date
if race is not None and race_id is None:
return datetime.now() > race.date
raise Exception("race_has_started received illegal arguments")
def find_first_else_none(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T | None: def find_first_else_none(predicate: Callable[[_T], bool], iterable: Iterable[_T]) -> _T | None:
""" """

View File

View File

@ -1,4 +1,4 @@
from typing import List, Callable, Dict, overload from typing import Callable, Dict, List, overload
from sqlalchemy import desc from sqlalchemy import desc
from formula10.database.model.db_driver import DbDriver from formula10.database.model.db_driver import DbDriver
@ -6,53 +6,34 @@ from formula10.database.model.db_race import DbRace
from formula10.database.model.db_race_guess import DbRaceGuess from formula10.database.model.db_race_guess import DbRaceGuess
from formula10.database.model.db_race_result import DbRaceResult from formula10.database.model.db_race_result import DbRaceResult
from formula10.database.model.db_season_guess import DbSeasonGuess from formula10.database.model.db_season_guess import DbSeasonGuess
from formula10.database.model.db_season_guess_result import DbSeasonGuessResult
from formula10.database.model.db_team import DbTeam from formula10.database.model.db_team import DbTeam
from formula10.database.model.db_user import DbUser from formula10.database.model.db_user import DbUser
from formula10.frontend.model.driver import NONE_DRIVER, Driver from formula10.database.validation import find_multiple_strict, find_single_or_none_strict, find_single_strict
from formula10.frontend.model.race import Race from formula10.domain.model.driver import NONE_DRIVER, Driver
from formula10.frontend.model.race_guess import RaceGuess from formula10.domain.model.race import Race
from formula10.frontend.model.race_result import RaceResult from formula10.domain.model.race_guess import RaceGuess
from formula10.frontend.model.season_guess import SeasonGuess from formula10.domain.model.race_result import RaceResult
from formula10.frontend.model.team import NONE_TEAM, Team from formula10.domain.model.season_guess import SeasonGuess
from formula10.frontend.model.user import User from formula10.domain.model.season_guess_result import SeasonGuessResult
from formula10.database.validation import find_first_else_none, find_multiple_strict, find_single_strict, find_single_or_none_strict from formula10.domain.model.team import NONE_TEAM, Team
from formula10.domain.model.user import User
from formula10 import db from formula10 import db
class TemplateModel: class Model():
"""
This class bundles all data required from inside a template.
"""
_all_users: List[User] | None = None _all_users: List[User] | None = None
_all_race_results: List[RaceResult] | None = None _all_race_results: List[RaceResult] | None = None
_all_race_guesses: List[RaceGuess] | None = None _all_race_guesses: List[RaceGuess] | None = None
_all_season_guesses: List[SeasonGuess] | None = None _all_season_guesses: List[SeasonGuess] | None = None
_all_season_guess_results: List[SeasonGuessResult] | None = None
_all_races: List[Race] | None = None _all_races: List[Race] | None = None
_all_drivers: List[Driver] | None = None _all_drivers: List[Driver] | None = None
_all_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 enabled users.
""" """
if self._all_users is None: if self._all_users is None:
self._all_users = [ self._all_users = [
@ -62,11 +43,98 @@ class TemplateModel:
return self._all_users return self._all_users
def all_users_or_active_user(self) -> List[User]: def all_race_results(self) -> List[RaceResult]:
if self.active_user is not None: """
return [self.active_user] Returns a list of all race results, in descending order (most recent first).
"""
if self._all_race_results is None:
self._all_race_results = [
RaceResult.from_db_race_result(db_race_result)
for db_race_result in db.session.query(DbRaceResult).join(DbRaceResult.race).order_by(desc(DbRace.number)).all()
]
return self.all_users() return self._all_race_results
def all_race_guesses(self) -> List[RaceGuess]:
"""
Returns a list of all race guesses (of enabled users).
"""
if self._all_race_guesses is None:
self._all_race_guesses = [
RaceGuess.from_db_race_guess(db_race_guess)
for db_race_guess in db.session.query(DbRaceGuess).join(DbRaceGuess.user).filter_by(enabled=True).all() # Ignore disabled users
]
return self._all_race_guesses
def all_season_guesses(self) -> List[SeasonGuess]:
"""
Returns a list of all season guesses (of enabled users).
"""
if self._all_season_guesses is None:
self._all_season_guesses = [
SeasonGuess.from_db_season_guess(db_season_guess)
for db_season_guess in db.session.query(DbSeasonGuess).join(DbSeasonGuess.user).filter_by(enabled=True).all() # Ignore disabled users
]
return self._all_season_guesses
def all_season_guess_results(self) -> List[SeasonGuessResult]:
if self._all_season_guess_results is None:
self._all_season_guess_results = [
SeasonGuessResult.from_db_season_guess_result(db_season_guess_result)
for db_season_guess_result in db.session.query(DbSeasonGuessResult).join(DbSeasonGuessResult.user).filter_by(enabled=True).all() # Ignore disabled users
]
return self._all_season_guess_results
def all_races(self) -> List[Race]:
"""
Returns a list of all races, in descending order (last race first).
"""
if self._all_races is None:
self._all_races = [
Race.from_db_race(db_race)
for db_race in db.session.query(DbRace).order_by(desc(DbRace.number)).all()
]
return self._all_races
def all_drivers(self, *, include_none: bool) -> List[Driver]:
"""
Returns a list of all drivers.
"""
if self._all_drivers is None:
self._all_drivers = [
Driver.from_db_driver(db_driver)
for db_driver in db.session.query(DbDriver).all()
]
if include_none:
return self._all_drivers
else:
predicate: Callable[[Driver], bool] = lambda driver: driver != NONE_DRIVER
return find_multiple_strict(predicate, self._all_drivers)
def all_teams(self, *, include_none: bool) -> List[Team]:
"""
Returns a list of all teams.
"""
if self._all_teams is None:
self._all_teams = [
Team.from_db_team(db_team)
for db_team in db.session.query(DbTeam).all()
]
if include_none:
return self._all_teams
else:
predicate: Callable[[Team], bool] = lambda team: team != NONE_TEAM
return find_multiple_strict(predicate, self._all_teams)
#
# User queries
#
@overload @overload
def user_by(self, *, user_name: str) -> User: def user_by(self, *, user_name: str) -> User:
@ -92,17 +160,9 @@ class TemplateModel:
predicate: Callable[[User], bool] = lambda user: user.name == user_name predicate: Callable[[User], bool] = lambda user: user.name == user_name
return find_single_strict(predicate, self.all_users()) return find_single_strict(predicate, self.all_users())
def all_race_results(self) -> List[RaceResult]: #
""" # Race result queries
Returns a list of all race results in the database, in descending order (most recent first). #
"""
if self._all_race_results is None:
self._all_race_results = [
RaceResult.from_db_race_result(db_race_result)
for db_race_result in db.session.query(DbRaceResult).join(DbRaceResult.race).order_by(desc(DbRace.number)).all()
]
return self._all_race_results
def race_result_by(self, *, race_name: str) -> RaceResult | None: def race_result_by(self, *, race_name: str) -> RaceResult | None:
""" """
@ -111,17 +171,9 @@ class TemplateModel:
predicate: Callable[[RaceResult], bool] = lambda result: result.race.name == race_name predicate: Callable[[RaceResult], bool] = lambda result: result.race.name == race_name
return find_single_or_none_strict(predicate, self.all_race_results()) return find_single_or_none_strict(predicate, self.all_race_results())
def all_race_guesses(self) -> List[RaceGuess]: #
""" # Race guess queries
Returns a list of all race guesses in the database. #
"""
if self._all_race_guesses is None:
self._all_race_guesses = [
RaceGuess.from_db_race_guess(db_race_guess)
for db_race_guess in db.session.query(DbRaceGuess).join(DbRaceGuess.user).filter_by(enabled=True).all() # Ignore disabled users
]
return self._all_race_guesses
@overload @overload
def race_guesses_by(self, *, user_name: str) -> List[RaceGuess]: def race_guesses_by(self, *, user_name: str) -> List[RaceGuess]:
@ -182,17 +234,12 @@ class TemplateModel:
raise Exception("race_guesses_by encountered illegal combination of arguments") raise Exception("race_guesses_by encountered illegal combination of arguments")
def all_season_guesses(self) -> List[SeasonGuess]: #
if self._all_season_guesses is None: # Season guess queries
self._all_season_guesses = [ #
SeasonGuess.from_db_season_guess(db_season_guess)
for db_season_guess in db.session.query(DbSeasonGuess).join(DbSeasonGuess.user).filter_by(enabled=True).all() # Ignore disabled users
]
return self._all_season_guesses
@overload @overload
def season_guesses_by(self, *, user_name: str) -> SeasonGuess: def season_guesses_by(self, *, user_name: str) -> SeasonGuess | None:
""" """
Returns the season guess made by a specific user. Returns the season guess made by a specific user.
""" """
@ -221,92 +268,24 @@ class TemplateModel:
raise Exception("season_guesses_by encountered illegal combination of arguments") raise Exception("season_guesses_by encountered illegal combination of arguments")
def all_races(self) -> List[Race]: #
""" # Season guess result queries
Returns a list of all races in the database. #
"""
if self._all_races is None:
self._all_races = [
Race.from_db_race(db_race)
for db_race in db.session.query(DbRace).order_by(desc(DbRace.number)).all()
]
return self._all_races def season_guess_result_by(self, *, user_name: str) -> SeasonGuessResult | None:
predicate: Callable[[SeasonGuessResult], bool] = lambda guess: guess.user.name == user_name
return find_single_or_none_strict(predicate, self.all_season_guess_results())
def first_race_without_result(self) -> Race | None: #
""" # Team queries
Returns the first race-object with no associated race result. #
"""
results: List[RaceResult] = self.all_race_results()
if len(results) == 0:
return self.all_races()[-1] # all_races is sorted descending by number
most_recent_result: RaceResult = results[0]
predicate: Callable[[Race], bool] = lambda race: race.number == most_recent_result.race.number + 1
return find_first_else_none(predicate, self.all_races())
@property
def current_race(self) -> Race | None:
return self.first_race_without_result()
def active_result_race_name_or_current_race_name(self) -> str:
if self.active_result is not None:
return self.active_result.race.name
elif self.current_race is not None:
return self.current_race.name
else:
return self.all_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.
"""
if self._all_teams is None:
self._all_teams = [
Team.from_db_team(db_team)
for db_team in db.session.query(DbTeam).all()
]
if include_none:
return self._all_teams
else:
predicate: Callable[[Team], bool] = lambda team: team != NONE_TEAM
return find_multiple_strict(predicate, self._all_teams)
def none_team(self) -> Team: def none_team(self) -> Team:
return NONE_TEAM return NONE_TEAM
def all_drivers(self, *, include_none: bool) -> List[Driver]: #
""" # Driver queries
Returns a list of all drivers in the database. #
"""
if self._all_drivers is None:
self._all_drivers = [
Driver.from_db_driver(db_driver)
for db_driver in db.session.query(DbDriver).all()
]
if include_none:
return self._all_drivers
else:
predicate: Callable[[Driver], bool] = lambda driver: driver != NONE_DRIVER
return find_multiple_strict(predicate, self._all_drivers)
def all_drivers_or_active_result_standing_drivers(self) -> List[Driver]:
return self.active_result.ordered_standing_list() if self.active_result is not None else self.all_drivers(include_none=False)
def drivers_for_wdc_gained(self) -> List[Driver]:
predicate: Callable[[Driver], bool] = lambda driver: driver.abbr not in self._wdc_gained_excluded_abbrs
return find_multiple_strict(predicate, self.all_drivers(include_none=False))
def none_driver(self) -> Driver: def none_driver(self) -> Driver:
return NONE_DRIVER return NONE_DRIVER
@ -343,3 +322,14 @@ class TemplateModel:
return drivers_by return drivers_by
raise Exception("drivers_by encountered illegal combination of arguments") raise Exception("drivers_by encountered illegal combination of arguments")
#
# Race queries
#
def race_by(self, *, race_name: str) -> Race:
for race in self.all_races():
if race.name == race_name:
return race
raise Exception(f"Couldn't find race {race_name}")

View File

View File

@ -1,13 +1,14 @@
from urllib.parse import quote from urllib.parse import quote
from formula10.database.model.db_driver import DbDriver from formula10.database.model.db_driver import DbDriver
from formula10.frontend.model.team import NONE_TEAM, Team from formula10.domain.model.team import NONE_TEAM, Team
class Driver(): class Driver():
@classmethod @classmethod
def from_db_driver(cls, db_driver: DbDriver): def from_db_driver(cls, db_driver: DbDriver):
driver: Driver = cls() driver: Driver = cls()
driver.id = db_driver.id
driver.name = db_driver.name driver.name = db_driver.name
driver.abbr = db_driver.abbr driver.abbr = db_driver.abbr
driver.country = db_driver.country_code driver.country = db_driver.country_code
@ -15,18 +16,23 @@ class Driver():
return driver return driver
def to_db_driver(self) -> DbDriver: def to_db_driver(self) -> DbDriver:
db_driver: DbDriver = DbDriver(name=self.name) db_driver: DbDriver = DbDriver(id=self.id)
db_driver.name = self.name
db_driver.abbr = self.abbr db_driver.abbr = self.abbr
db_driver.country_code = self.country db_driver.country_code = self.country
db_driver.team_name = self.team.name db_driver.team_id = self.team.name
return db_driver return db_driver
def __eq__(self, __value: object) -> bool: def __eq__(self, __value: object) -> bool:
if isinstance(__value, Driver): if isinstance(__value, Driver):
return self.name == __value.name return self.id == __value.id
return NotImplemented return NotImplemented
def __hash__(self) -> int:
return hash(self.id)
id: int
name: str name: str
abbr: str abbr: str
country: str country: str
@ -38,6 +44,7 @@ class Driver():
NONE_DRIVER: Driver = Driver() NONE_DRIVER: Driver = Driver()
NONE_DRIVER.id = 0
NONE_DRIVER.name = "None" NONE_DRIVER.name = "None"
NONE_DRIVER.abbr = "None" NONE_DRIVER.abbr = "None"
NONE_DRIVER.country = "NO" NONE_DRIVER.country = "NO"

View File

@ -8,6 +8,7 @@ class Race():
@classmethod @classmethod
def from_db_race(cls, db_race: DbRace): def from_db_race(cls, db_race: DbRace):
race: Race = cls() race: Race = cls()
race.id = db_race.id
race.name = db_race.name race.name = db_race.name
race.number = db_race.number race.number = db_race.number
race.date = db_race.date race.date = db_race.date
@ -15,18 +16,23 @@ class Race():
return race return race
def to_db_race(self) -> DbRace: def to_db_race(self) -> DbRace:
db_race: DbRace = DbRace(name=self.name, db_race: DbRace = DbRace(id=self.id)
number=self.number, db_race.name = self.name
date=self.date, db_race.number = self.number
pxx=self.place_to_guess) db_race.date = self.date
db_race.pxx = self.place_to_guess
return db_race return db_race
def __eq__(self, __value: object) -> bool: def __eq__(self, __value: object) -> bool:
if isinstance(__value, Race): if isinstance(__value, Race):
return self.name == __value.name return self.id == __value.id
return NotImplemented return NotImplemented
def __hash__(self) -> int:
return hash(self.id)
id: int
name: str name: str
number: int number: int
date: datetime date: datetime

View File

@ -1,7 +1,7 @@
from formula10.database.model.db_race_guess import DbRaceGuess from formula10.database.model.db_race_guess import DbRaceGuess
from formula10.frontend.model.driver import Driver from formula10.domain.model.driver import Driver
from formula10.frontend.model.race import Race from formula10.domain.model.race import Race
from formula10.frontend.model.user import User from formula10.domain.model.user import User
class RaceGuess(): class RaceGuess():
@ -15,10 +15,9 @@ class RaceGuess():
return race_guess return race_guess
def to_db_race_guess(self) -> DbRaceGuess: def to_db_race_guess(self) -> DbRaceGuess:
db_race_guess: DbRaceGuess = DbRaceGuess(user_name=self.user.name, db_race_guess: DbRaceGuess = DbRaceGuess(user_id=self.user.id, race_id=self.race.id)
race_name=self.race.name, db_race_guess.pxx_driver_id = self.pxx_guess.id
pxx_driver_name=self.pxx_guess.name, db_race_guess.dnf_driver_id = self.dnf_guess.id
dnf_driver_name=self.dnf_guess.name)
return db_race_guess return db_race_guess
def __eq__(self, __value: object) -> bool: def __eq__(self, __value: object) -> bool:
@ -27,6 +26,9 @@ class RaceGuess():
return NotImplemented return NotImplemented
def __hash__(self) -> int:
return hash((self.user, self.race))
user: User user: User
race: Race race: Race
pxx_guess: Driver pxx_guess: Driver

View File

@ -3,8 +3,8 @@ from typing import Dict, List
from formula10.database.common_queries import find_single_driver_strict from formula10.database.common_queries import find_single_driver_strict
from formula10.database.model.db_race_result import DbRaceResult from formula10.database.model.db_race_result import DbRaceResult
from formula10.frontend.model.driver import NONE_DRIVER, Driver from formula10.domain.model.driver import NONE_DRIVER, Driver
from formula10.frontend.model.race import Race from formula10.domain.model.race import Race
class RaceResult: class RaceResult:
@ -12,30 +12,41 @@ class RaceResult:
def from_db_race_result(cls, db_race_result: DbRaceResult): def from_db_race_result(cls, db_race_result: DbRaceResult):
race_result: RaceResult = cls() race_result: RaceResult = cls()
race_result.race = Race.from_db_race(db_race_result.race) race_result.race = Race.from_db_race(db_race_result.race)
race_result.fastest_lap_driver = Driver.from_db_driver(db_race_result.fastest_lap_driver)
# Deserialize from json # Deserialize from json
standing: Dict[str, str] = json.loads(db_race_result.pxx_driver_names_json) standing: Dict[str, str] = json.loads(db_race_result.pxx_driver_ids_json)
initial_dnf: List[str] = json.loads(db_race_result.first_dnf_driver_names_json) initial_dnf: List[str] = json.loads(db_race_result.first_dnf_driver_ids_json)
all_dnfs: List[str] = json.loads(db_race_result.dnf_driver_names_json) all_dnfs: List[str] = json.loads(db_race_result.dnf_driver_ids_json)
standing_exclusions: List[str] = json.loads(db_race_result.excluded_driver_names_json) standing_exclusions: List[str] = json.loads(db_race_result.excluded_driver_ids_json)
sprint_dnfs: List[str] = json.loads(db_race_result.sprint_dnf_driver_ids_json)
sprint_standing: Dict[str, str] = json.loads(db_race_result.sprint_points_json)
# Populate relationships # Populate relationships
race_result.standing = { race_result.standing = {
position: Driver.from_db_driver(find_single_driver_strict(driver_name)) position: Driver.from_db_driver(find_single_driver_strict(int(driver_id)))
for position, driver_name in standing.items() for position, driver_id in standing.items()
} }
race_result.initial_dnf = [ race_result.initial_dnf = [
Driver.from_db_driver(find_single_driver_strict(driver_name)) Driver.from_db_driver(find_single_driver_strict(int(driver_id)))
for driver_name in initial_dnf for driver_id in initial_dnf
] ]
race_result.all_dnfs = [ race_result.all_dnfs = [
Driver.from_db_driver(find_single_driver_strict(driver_name)) Driver.from_db_driver(find_single_driver_strict(int(driver_id)))
for driver_name in all_dnfs for driver_id in all_dnfs
] ]
race_result.standing_exclusions = [ race_result.standing_exclusions = [
Driver.from_db_driver(find_single_driver_strict(driver_name)) Driver.from_db_driver(find_single_driver_strict(int(driver_id)))
for driver_name in standing_exclusions for driver_id in standing_exclusions
] ]
race_result.sprint_dnfs = [
Driver.from_db_driver(find_single_driver_strict(int(driver_id)))
for driver_id in sprint_dnfs
]
race_result.sprint_standing = {
position: Driver.from_db_driver(find_single_driver_strict(int(driver_id)))
for position, driver_id in sprint_standing.items()
}
return race_result return race_result
@ -45,21 +56,30 @@ class RaceResult:
position: driver.name for position, driver in self.standing.items() position: driver.name for position, driver in self.standing.items()
} }
initial_dnf: List[str] = [ initial_dnf: List[str] = [
driver.name for driver in self.initial_dnf if driver str(driver.id) for driver in self.initial_dnf if driver
] ]
all_dnfs: List[str] = [ all_dnfs: List[str] = [
driver.name for driver in self.all_dnfs if driver str(driver.id) for driver in self.all_dnfs if driver
] ]
standing_exclusions: List[str] = [ standing_exclusions: List[str] = [
driver.name for driver in self.standing_exclusions if driver str(driver.id) for driver in self.standing_exclusions if driver
] ]
sprint_dnfs: List[str] = [
str(driver.id) for driver in self.sprint_dnfs if driver
]
sprint_standing: Dict[str, str] = {
position: driver.name for position, driver in self.sprint_standing.items()
}
# Serialize to json # Serialize to json
db_race_result: DbRaceResult = DbRaceResult(race_name=self.race.name, db_race_result: DbRaceResult = DbRaceResult(race_id=self.race.id)
pxx_driver_names_json=json.dumps(standing), db_race_result.pxx_driver_ids_json = json.dumps(standing)
first_dnf_driver_names_json=json.dumps(initial_dnf), db_race_result.first_dnf_driver_ids_json = json.dumps(initial_dnf)
dnf_driver_names_json=json.dumps(all_dnfs), db_race_result.dnf_driver_ids_json = json.dumps(all_dnfs)
excluded_driver_names_json=json.dumps(standing_exclusions)) db_race_result.excluded_driver_ids_json = json.dumps(standing_exclusions)
db_race_result.fastest_lap_id = self.fastest_lap_driver.id
db_race_result.sprint_dnf_driver_ids_json = json.dumps(sprint_dnfs)
db_race_result.sprint_points_json = json.dumps(sprint_standing)
return db_race_result return db_race_result
@ -69,12 +89,19 @@ class RaceResult:
return NotImplemented return NotImplemented
def __hash__(self) -> int:
return hash(self.race)
race: Race race: Race
standing: Dict[str, Driver] standing: Dict[str, Driver] # Always contains all 20 drivers, even if DNF'ed or excluded
initial_dnf: List[Driver] initial_dnf: List[Driver] # initial_dnf is empty if no-one DNF'ed
all_dnfs: List[Driver] all_dnfs: List[Driver]
standing_exclusions: List[Driver] standing_exclusions: List[Driver]
fastest_lap_driver: Driver
sprint_dnfs: List[Driver]
sprint_standing: Dict[str, Driver]
def offset_from_place_to_guess(self, offset: int, respect_nc:bool = True) -> Driver: def offset_from_place_to_guess(self, offset: int, respect_nc:bool = True) -> Driver:
position: str = str(self.race.place_to_guess + offset) position: str = str(self.race.place_to_guess + offset)
@ -86,6 +113,16 @@ class RaceResult:
return self.standing[position] return self.standing[position]
def driver_standing_position(self, driver: Driver) -> int | None:
if driver == NONE_DRIVER:
return None
for position, _driver in self.standing.items():
if driver == _driver and driver not in self.standing_exclusions:
return int(position)
return None
def driver_standing_position_string(self, driver: Driver) -> str: def driver_standing_position_string(self, driver: Driver) -> str:
if driver == NONE_DRIVER: if driver == NONE_DRIVER:
return "" return ""

View File

@ -2,9 +2,9 @@ import json
from typing import List from typing import List
from formula10.database.common_queries import find_single_driver_strict from formula10.database.common_queries import find_single_driver_strict
from formula10.database.model.db_season_guess import DbSeasonGuess from formula10.database.model.db_season_guess import DbSeasonGuess
from formula10.frontend.model.driver import Driver from formula10.domain.model.driver import Driver
from formula10.frontend.model.team import Team from formula10.domain.model.team import Team
from formula10.frontend.model.user import User from formula10.domain.model.user import User
class SeasonGuess(): class SeasonGuess():
@ -20,17 +20,17 @@ class SeasonGuess():
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 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 # Deserialize from json
team_winners: List[str | None] = json.loads(db_season_guess.team_winners_driver_names_json) team_winners: List[str | None] = json.loads(db_season_guess.team_winners_driver_ids_json)
podiums: List[str] = json.loads(db_season_guess.podium_drivers_driver_names_json) podiums: List[str] = json.loads(db_season_guess.podium_drivers_driver_ids_json)
# Populate relationships # Populate relationships
season_guess.team_winners = [ season_guess.team_winners = [
Driver.from_db_driver(find_single_driver_strict(driver_name)) if driver_name is not None else None Driver.from_db_driver(find_single_driver_strict(int(driver_id))) if driver_id is not None else None
for driver_name in team_winners for driver_id in team_winners
] ]
season_guess.podiums = [ season_guess.podiums = [
Driver.from_db_driver(find_single_driver_strict(driver_name)) Driver.from_db_driver(find_single_driver_strict(int(driver_id)))
for driver_name in podiums for driver_id in podiums
] ]
return season_guess return season_guess
@ -46,19 +46,27 @@ class SeasonGuess():
] ]
# Serialize to json # Serialize to json
db_season_guess: DbSeasonGuess = DbSeasonGuess(user_name=self.user.name, db_season_guess: DbSeasonGuess = DbSeasonGuess(user_id=self.user.id)
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.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.p2_team_id = self.p2_wcc.id 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.overtake_driver_id = self.most_overtakes.id 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.dnf_driver_id = self.most_dnfs.id 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.gained_driver_id = self.most_wdc_gained.id 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 db_season_guess.lost_driver_id = self.most_wdc_lost.id if self.most_wdc_lost is not None else None
db_season_guess.team_winners_driver_ids_json=json.dumps(team_winners)
db_season_guess.podium_drivers_driver_ids_json=json.dumps(podiums)
return db_season_guess return db_season_guess
def __eq__(self, __value: object) -> bool:
if isinstance(__value, SeasonGuess):
return self.user == __value.user
return NotImplemented
def __hash__(self) -> int:
return hash(self.user)
user: User user: User
hot_take: str | None hot_take: str | None
p2_wcc: Team | None p2_wcc: Team | None

View File

@ -0,0 +1,32 @@
from formula10.database.model.db_season_guess_result import DbSeasonGuessResult
from formula10.domain.model.user import User
class SeasonGuessResult():
@classmethod
def from_db_season_guess_result(cls, db_season_guess_result: DbSeasonGuessResult):
season_guess_result: SeasonGuessResult = cls()
season_guess_result.user = User.from_db_user(db_season_guess_result.user)
season_guess_result.hot_take_correct = db_season_guess_result.hot_take_correct
season_guess_result.overtakes_correct = db_season_guess_result.overtakes_correct
return season_guess_result
def to_db_season_guess_result(self) -> DbSeasonGuessResult:
db_season_guess_result: DbSeasonGuessResult = DbSeasonGuessResult(user_id=self.user.id)
db_season_guess_result.hot_take_correct = self.hot_take_correct
db_season_guess_result.overtakes_correct = self.overtakes_correct
return db_season_guess_result
def __eq__(self, __value: object) -> bool:
if isinstance(__value, SeasonGuessResult):
return self.user == __value.user
return NotImplemented
def __hash__(self) -> int:
return hash(self.user)
user: User
hot_take_correct: bool
overtakes_correct: bool

View File

@ -7,19 +7,25 @@ class Team():
@classmethod @classmethod
def from_db_team(cls, db_team: DbTeam): def from_db_team(cls, db_team: DbTeam):
team: Team = cls() team: Team = cls()
team.id = db_team.id
team.name = db_team.name team.name = db_team.name
return team return team
def to_db_team(self) -> DbTeam: def to_db_team(self) -> DbTeam:
db_team: DbTeam = DbTeam(name=self.name) db_team: DbTeam = DbTeam(id=self.id)
db_team.name = self.name
return db_team return db_team
def __eq__(self, __value: object) -> bool: def __eq__(self, __value: object) -> bool:
if isinstance(__value, Team): if isinstance(__value, Team):
return self.name == __value.name return self.id == __value.id
return NotImplemented return NotImplemented
def __hash__(self) -> int:
return hash(self.id)
id: int
name: str name: str
@property @property
@ -27,4 +33,5 @@ class Team():
return quote(self.name) return quote(self.name)
NONE_TEAM: Team = Team() NONE_TEAM: Team = Team()
NONE_TEAM.id = 0
NONE_TEAM.name = "None" NONE_TEAM.name = "None"

View File

@ -7,20 +7,27 @@ class User():
@classmethod @classmethod
def from_db_user(cls, db_user: DbUser): def from_db_user(cls, db_user: DbUser):
user: User = cls() user: User = cls()
user.id = db_user.id
user.name = db_user.name user.name = db_user.name
user.enabled = db_user.enabled user.enabled = db_user.enabled
return user return user
def to_db_user(self) -> DbUser: def to_db_user(self) -> DbUser:
db_user: DbUser = DbUser(name=self.name, enabled=self.enabled) db_user: DbUser = DbUser(id=self.id)
db_user.name = self.name
db_user.enabled = self.enabled
return db_user return db_user
def __eq__(self, __value: object) -> bool: def __eq__(self, __value: object) -> bool:
if isinstance(__value, User): if isinstance(__value, User):
return self.name == __value.name return self.id == __value.id
return NotImplemented return NotImplemented
def __hash__(self) -> int:
return hash(self.id)
id: int
name: str name: str
enabled: bool enabled: bool

View File

@ -0,0 +1,624 @@
import json
from typing import Any, Callable, Dict, List, overload
import numpy as np
from formula10.domain.domain_model import Model
from formula10.domain.model.driver import NONE_DRIVER, Driver
from formula10.domain.model.race_guess import RaceGuess
from formula10.domain.model.race_result import RaceResult
from formula10.domain.model.season_guess import SeasonGuess
from formula10.domain.model.season_guess_result import SeasonGuessResult
from formula10.domain.model.team import Team
from formula10.domain.model.user import User
RACE_GUESS_OFFSET_POINTS: Dict[int, int] = {
3: 1,
2: 3,
1: 6,
0: 10
}
RACE_GUESS_DNF_POINTS: int = 10
SEASON_GUESS_HOT_TAKE_POINTS: int = 10
SEASON_GUESS_P2_POINTS: int = 10
SEASON_GUESS_OVERTAKES_POINTS: int = 10
SEASON_GUESS_DNF_POINTS: int = 10
SEASON_GUESS_GAINED_POINTS: int = 10
SEASON_GUESS_LOST_POINTS: int = 10
SEASON_GUESS_TEAMWINNER_CORRECT_POINTS: int = 3
SEASON_GUESS_TEAMWINNER_FALSE_POINTS: int = -3
SEASON_GUESS_PODIUMS_CORRECT_POINTS: int = 3
SEASON_GUESS_PODIUMS_FALSE_POINTS: int = -2
DRIVER_RACE_POINTS: Dict[int, int] = {
1: 25,
2: 18,
3: 15,
4: 12,
5: 10,
6: 8,
7: 6,
8: 4,
9: 2,
10: 1
}
WDC_STANDING_2023: Dict[str, int] = {
"Max Verstappen": 1,
"Sergio Perez": 2,
"Lewis Hamilton": 3,
"Fernando Alonso": 4,
"Charles Leclerc": 5,
"Lando Norris": 6,
"Carlos Sainz": 7,
"George Russell": 8,
"Oscar Piastri": 9,
"Lance Stroll": 10,
"Pierre Gasly": 11,
"Esteban Ocon": 12,
"Alexander Albon": 13,
"Yuki Tsunoda": 14,
"Valtteri Bottas": 15,
"Nico Hulkenberg": 16,
"Daniel Ricciardo": 17,
"Zhou Guanyu": 18,
"Kevin Magnussen": 19,
"Logan Sargeant": 21
}
WCC_STANDING_2023: Dict[str, int] = {
"Red Bull": 1,
"Mercedes": 2,
"Ferrari": 3,
"McLaren": 4,
"Aston Martin": 5,
"Alpine": 6,
"Williams": 7,
"VCARB": 8,
"Sauber": 9,
"Haas": 10
}
def standing_points(race_guess: RaceGuess, race_result: RaceResult) -> int:
guessed_driver_position: int | None = race_result.driver_standing_position(driver=race_guess.pxx_guess)
if guessed_driver_position is None:
return 0
position_offset: int = abs(guessed_driver_position - race_guess.race.place_to_guess)
if position_offset not in RACE_GUESS_OFFSET_POINTS:
return 0
return RACE_GUESS_OFFSET_POINTS[position_offset]
def dnf_points(race_guess: RaceGuess, race_result: RaceResult) -> int:
if race_guess.dnf_guess in race_result.initial_dnf:
return RACE_GUESS_DNF_POINTS
if race_guess.dnf_guess == NONE_DRIVER and len(race_result.initial_dnf) == 0:
return RACE_GUESS_DNF_POINTS
return 0
class PointsModel(Model):
"""
This class bundles all data + functionality required to do points calculations.
"""
_points_per_step: Dict[str, List[int]] | None = None
_driver_points_per_step: Dict[str, List[int]] | None = None
_team_points_per_step: Dict[str, List[int]] | None = None
_dnfs: Dict[str, int] | None = None
def __init__(self):
Model.__init__(self)
def points_per_step(self) -> Dict[str, List[int]]:
"""
Returns a dictionary of lists, containing points per race for each user.
"""
if self._points_per_step is None:
self._points_per_step = dict()
for user in self.all_users():
self._points_per_step[user.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers
for race_guess in self.all_race_guesses():
user_name: str = race_guess.user.name
race_number: int = race_guess.race.number
race_result: RaceResult | None = self.race_result_by(race_name=race_guess.race.name)
if race_result is None:
continue
self._points_per_step[user_name][race_number] = standing_points(race_guess, race_result) + dnf_points(race_guess, race_result)
return self._points_per_step
# @todo Doesn't include fastest lap + sprint points
def driver_points_per_step(self) -> Dict[str, List[int]]:
"""
Returns a dictionary of lists, containing points per race for each driver.
"""
if self._driver_points_per_step is None:
self._driver_points_per_step = dict()
for driver in self.all_drivers(include_none=False):
self._driver_points_per_step[driver.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers
for race_result in self.all_race_results():
for position, driver in race_result.standing.items():
driver_name: str = driver.name
race_number: int = race_result.race.number
self._driver_points_per_step[driver_name][race_number] = DRIVER_RACE_POINTS[int(position)] if int(position) in DRIVER_RACE_POINTS else 0
return self._driver_points_per_step
# @todo Doesn't include fastest lap + sprint points
def team_points_per_step(self) -> Dict[str, List[int]]:
"""
Returns a dictionary of lists, containing points per race for each team.
"""
if self._team_points_per_step is None:
self._team_points_per_step = dict()
for team in self.all_teams(include_none=False):
self._team_points_per_step[team.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers
for race_result in self.all_race_results():
for position, driver in race_result.standing.items():
team_name: str = driver.team.name
race_number: int = race_result.race.number
self._team_points_per_step[team_name][race_number] += DRIVER_RACE_POINTS[int(position)] if int(position) in DRIVER_RACE_POINTS else 0
return self._team_points_per_step
# @todo Doesn't include sprint dnfs
def dnfs(self) -> Dict[str, int]:
if self._dnfs is None:
self._dnfs = dict()
for driver in self.all_drivers(include_none=False):
self._dnfs[driver.name] = 0
for race_result in self.all_race_results():
for driver in race_result.all_dnfs:
self._dnfs[driver.name] += 1
return self._dnfs
#
# Driver stats
#
def driver_points_per_step_cumulative(self) -> Dict[str, List[int]]:
"""
Returns a dictionary of lists, containing cumulative points per race for each driver.
"""
points_per_step_cumulative: Dict[str, List[int]] = dict()
for driver_name, points in self.driver_points_per_step().items():
points_per_step_cumulative[driver_name] = np.cumsum(points).tolist()
return points_per_step_cumulative
@overload
def driver_points_by(self, *, driver_name: str) -> List[int]:
"""
Returns a list of points per race for a specific driver.
"""
return self.driver_points_by(driver_name=driver_name)
@overload
def driver_points_by(self, *, race_name: str) -> Dict[str, int]:
"""
Returns a dictionary of points per driver for a specific race.
"""
return self.driver_points_by(race_name=race_name)
@overload
def driver_points_by(self, *, driver_name: str, race_name: str) -> int:
"""
Returns the points for a specific race for a specific driver.
"""
return self.driver_points_by(driver_name=driver_name, race_name=race_name)
def driver_points_by(self, *, driver_name: str | None = None, race_name: str | None = None) -> List[int] | Dict[str, int] | int:
if driver_name is not None and race_name is None:
return self.driver_points_per_step()[driver_name]
if driver_name is None and race_name is not None:
race_number: int = self.race_by(race_name=race_name).number
points_by_race: Dict[str, int] = dict()
for _driver_name, points in self.driver_points_per_step().items():
points_by_race[_driver_name] = points[race_number]
return points_by_race
if driver_name is not None and race_name is not None:
race_number: int = self.race_by(race_name=race_name).number
return self.driver_points_per_step()[driver_name][race_number]
raise Exception("driver_points_by received an illegal combination of arguments")
def total_driver_points_by(self, driver_name: str) -> int:
return sum(self.driver_points_by(driver_name=driver_name))
def drivers_sorted_by_points(self) -> List[Driver]:
comparator: Callable[[Driver], int] = lambda driver: self.total_driver_points_by(driver.name)
return sorted(self.all_drivers(include_none=False), key=comparator, reverse=True)
def wdc_standing_by_position(self) -> Dict[int, List[str]]:
standing: Dict[int, List[str]] = dict()
for position in range(1, len(self.all_drivers(include_none=False)) + 1):
standing[position] = list()
position: int = 1
last_points: int = 0
for driver in self.drivers_sorted_by_points():
points: int = self.total_driver_points_by(driver.name)
if points < last_points:
position += 1
standing[position].append(driver.name)
last_points = points
return standing
def wdc_standing_by_driver(self) -> Dict[str, int]:
standing: Dict[str, int] = dict()
position: int = 1
last_points: int = 0
for driver in self.drivers_sorted_by_points():
points: int = self.total_driver_points_by(driver.name)
if points < last_points:
position += 1
standing[driver.name] = position
last_points = points
return standing
def wdc_diff_2023_by(self, driver_name: str) -> int:
return WDC_STANDING_2023[driver_name] - self.wdc_standing_by_driver()[driver_name]
def most_dnf_names(self) -> List[str]:
dnf_names: List[str] = list()
most_dnfs: int = 0
for dnfs in self.dnfs().values():
if dnfs > most_dnfs:
most_dnfs = dnfs
for driver_name, dnfs in self.dnfs().items():
if dnfs == most_dnfs:
dnf_names.append(driver_name)
return dnf_names
def most_gained_names(self) -> List[str]:
most_gained_names: List[str] = list()
most_gained: int = 0
for driver in self.all_drivers(include_none=False):
gained: int = self.wdc_diff_2023_by(driver.name)
if gained > most_gained:
most_gained = gained
for driver in self.all_drivers(include_none=False):
gained: int = self.wdc_diff_2023_by(driver.name)
if gained == most_gained:
most_gained_names.append(driver.name)
return most_gained_names
def most_lost_names(self) -> List[str]:
most_lost_names: List[str] = list()
most_lost: int = 100
for driver in self.all_drivers(include_none=False):
lost: int = self.wdc_diff_2023_by(driver.name)
if lost < most_lost:
most_lost = lost
for driver in self.all_drivers(include_none=False):
lost: int = self.wdc_diff_2023_by(driver.name)
if lost == most_lost:
most_lost_names.append(driver.name)
return most_lost_names
#
# Team points
#
def team_points_per_step_cumulative(self) -> Dict[str, List[int]]:
"""
Returns a dictionary of lists, containing cumulative points per race for each team.
"""
points_per_step_cumulative: Dict[str, List[int]] = dict()
for team_name, points in self.team_points_per_step().items():
points_per_step_cumulative[team_name] = np.cumsum(points).tolist()
return points_per_step_cumulative
def total_team_points_by(self, team_name: str) -> int:
teammates: List[Driver] = self.drivers_by(team_name=team_name)
return sum(self.driver_points_by(driver_name=teammates[0].name)) + sum(self.driver_points_by(driver_name=teammates[1].name))
def teams_sorted_by_points(self) -> List[Team]:
comparator: Callable[[Team], int] = lambda team: self.total_team_points_by(team.name)
return sorted(self.all_teams(include_none=False), key=comparator, reverse=True)
def wcc_standing_by_position(self) -> Dict[int, List[str]]:
standing: Dict[int, List[str]] = dict()
for position in range (1, len(self.all_teams(include_none=False)) + 1):
standing[position] = list()
position: int = 1
last_points: int = 0
for team in self.teams_sorted_by_points():
points: int = self.total_team_points_by(team.name)
if points < last_points:
position += 1
standing[position].append(team.name)
last_points = points
return standing
def wcc_standing_by_team(self) -> Dict[str, int]:
standing: Dict[str, int] = dict()
position: int = 1
last_points: int = 0
for team in self.teams_sorted_by_points():
points: int = self.total_team_points_by(team.name)
if points < last_points:
position += 1
standing[team.name] = position
last_points = points
return standing
def wcc_diff_2023_by(self, team_name: str) -> int:
return WCC_STANDING_2023[team_name] - self.wcc_standing_by_team()[team_name]
#
# User stats
#
def points_per_step_cumulative(self) -> Dict[str, List[int]]:
"""
Returns a dictionary of lists, containing cumulative points per race for each user.
"""
points_per_step_cumulative: Dict[str, List[int]] = dict()
for user_name, points in self.points_per_step().items():
points_per_step_cumulative[user_name] = np.cumsum(points).tolist()
return points_per_step_cumulative
@overload
def points_by(self, *, user_name: str) -> List[int]:
"""
Returns a list of points per race for a specific user.
"""
return self.points_by(user_name=user_name)
@overload
def points_by(self, *, race_name: str) -> Dict[str, int]:
"""
Returns a dictionary of points per user for a specific race.
"""
return self.points_by(race_name=race_name)
@overload
def points_by(self, *, user_name: str, race_name: str) -> int:
"""
Returns the points for a specific race for a specific user.
"""
return self.points_by(user_name=user_name, race_name=race_name)
def points_by(self, *, user_name: str | None = None, race_name: str | None = None) -> List[int] | Dict[str, int] | int:
if user_name is not None and race_name is None:
return self.points_per_step()[user_name]
if user_name is None and race_name is not None:
race_number: int = self.race_by(race_name=race_name).number
points_by_race: Dict[str, int] = dict()
for _user_name, points in self.points_per_step().items():
points_by_race[_user_name] = points[race_number]
return points_by_race
if user_name is not None and race_name is not None:
race_number: int = self.race_by(race_name=race_name).number
return self.points_per_step()[user_name][race_number]
raise Exception("points_by received an illegal combination of arguments")
def total_points_by(self, user_name: str) -> int:
"""
Returns the total number of points for a specific user.
"""
return sum(self.points_by(user_name=user_name))
def users_sorted_by_points(self) -> List[User]:
"""
Returns the list of users, sorted by their points from race guesses (in descending order).
"""
comparator: Callable[[User], int] = lambda user: self.total_points_by(user.name)
return sorted(self.all_users(), key=comparator, reverse=True)
def user_standing(self) -> Dict[str, int]:
standing: Dict[str, int] = dict()
position: int = 1
last_points: int = 0
for user in self.users_sorted_by_points():
if self.total_points_by(user.name) < last_points:
position += 1
standing[user.name] = position
last_points = self.total_points_by(user.name)
return standing
def picks_count(self, user_name: str) -> int:
# Treat standing + dnf picks separately
return len(self.race_guesses_by(user_name=user_name)) * 2
def picks_with_points_count(self, user_name: str) -> int:
count: int = 0
for race_guess in self.race_guesses_by(user_name=user_name):
race_result: RaceResult | None = self.race_result_by(race_name=race_guess.race.name)
if race_result is None:
continue
if standing_points(race_guess, race_result) > 0:
count += 1
if dnf_points(race_guess, race_result) > 0:
count += 1
return count
def points_per_pick(self, user_name: str) -> float:
if self.picks_count(user_name) == 0:
return 0.0
return self.total_points_by(user_name) / self.picks_count(user_name)
#
# Season guess evaluation
#
def hot_take_correct(self, user_name: str) -> bool:
season_guess_result: SeasonGuessResult | None = self.season_guess_result_by(user_name=user_name)
return season_guess_result.hot_take_correct if season_guess_result is not None else False
def p2_constructor_correct(self, user_name: str) -> bool:
season_guess: SeasonGuess | None = self.season_guesses_by(user_name=user_name)
if season_guess is None or season_guess.p2_wcc is None:
return False
return season_guess.p2_wcc.name in self.wcc_standing_by_position()[2]
def overtakes_correct(self, user_name: str) -> bool:
season_guess_result: SeasonGuessResult | None = self.season_guess_result_by(user_name=user_name)
return season_guess_result.overtakes_correct if season_guess_result is not None else False
def dnfs_correct(self, user_name: str) -> bool:
season_guess: SeasonGuess | None = self.season_guesses_by(user_name=user_name)
if season_guess is None or season_guess.most_dnfs is None:
return False
return season_guess.most_dnfs.name in self.most_dnf_names()
def most_gained_correct(self, user_name: str) -> bool:
season_guess: SeasonGuess | None = self.season_guesses_by(user_name=user_name)
if season_guess is None or season_guess.most_wdc_gained is None:
return False
return season_guess.most_wdc_gained.name in self.most_gained_names()
def most_lost_correct(self, user_name: str) -> bool:
season_guess: SeasonGuess | None = self.season_guesses_by(user_name=user_name)
if season_guess is None or season_guess.most_wdc_lost is None:
return False
return season_guess.most_wdc_lost.name in self.most_lost_names()
def is_team_winner(self, driver: Driver) -> bool:
teammates: List[Driver] = self.drivers_by(team_name=driver.team.name)
teammate: Driver = teammates[0] if teammates[1] == driver else teammates[1]
print(f"{driver.name} standing: {self.wdc_standing_by_driver()[driver.name]}, {teammate.name} standing: {self.wdc_standing_by_driver()[teammate.name]}")
return self.wdc_standing_by_driver()[driver.name] <= self.wdc_standing_by_driver()[teammate.name]
def has_podium(self, driver: Driver) -> bool:
for race_result in self.all_race_results():
position: int | None = race_result.driver_standing_position(driver)
if position is not None and position <= 3:
return True
return False
#
# Diagram queries
#
def cumulative_points_data(self) -> str:
data: Dict[Any, Any] = dict()
data["labels"] = [0] + [
race.name for race in sorted(self.all_races(), key=lambda race: race.number)
]
data["datasets"] = [
{
"data": self.points_per_step_cumulative()[user.name],
"label": user.name,
"fill": False
}
for user in self.all_users()
]
return json.dumps(data)
def cumulative_driver_points_data(self) -> str:
data: Dict[Any, Any] = dict()
data["labels"] = [0] + [
race.name for race in sorted(self.all_races(), key=lambda race: race.number)
]
data["datasets"] = [
{
"data": self.driver_points_per_step_cumulative()[driver.name],
"label": driver.abbr,
"fill": False
}
for driver in self.all_drivers(include_none=False)
]
return json.dumps(data)
def cumulative_team_points_data(self) -> str:
data: Dict[Any, Any] = dict()
data["labels"] = [0] + [
race.name for race in sorted(self.all_races(), key=lambda race: race.number)
]
data["datasets"] = [
{
"data": self.team_points_per_step_cumulative()[team.name],
"label": team.name,
"fill": False
}
for team in self.all_teams(include_none=False)
]
return json.dumps(data)

View File

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

View File

@ -0,0 +1,29 @@
// Set the date we're counting down to
var raceTime = document.getElementById("race_date").innerHTML
var countDownDate = new Date(raceTime).getTime();
// Update the countdown every 1 second
var x = setInterval(function() {
// Get today's date and time
var now = new Date().getTime();
// Find the distance between now and the countdown date
var distance = countDownDate - now;
// Time calculations for days, hours, minutes and seconds
var days = Math.floor(distance / (1000 * 60 * 60 * 24));
var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
var seconds = Math.floor((distance % (1000 * 60)) / 1000);
// Display the result in the element with id="demo"
document.getElementById("race_date_countdown").innerHTML = days + "d " + hours + "h " + minutes + "m " + seconds + "s ";
// If the countdown is finished, write some text
if (distance < 0) {
clearInterval(x);
document.getElementById("race_date_countdown").innerHTML = "GO GO GO";
}
}, 1000);

View File

@ -41,13 +41,15 @@ function handleDrop(e) {
// Don't do anything if dropping the same column we're dragging. // Don't do anything if dropping the same column we're dragging.
if (dragSrcEl != this) { if (dragSrcEl != this) {
// Set the source column's HTML to the HTML of the column we dropped on. // Set the source column's HTML to the HTML of the column we dropped on.
//alert(this.outerHTML); // alert(this.outerHTML);
//dragSrcEl.innerHTML = this.innerHTML; // dragSrcEl.innerHTML = this.innerHTML;
//this.innerHTML = e.dataTransfer.getData('text/html'); // this.innerHTML = e.dataTransfer.getData('text/html');
this.parentNode.removeChild(dragSrcEl); this.parentNode.removeChild(dragSrcEl);
let dropHTML = e.dataTransfer.getData('text/html'); let dropHTML = e.dataTransfer.getData('text/html');
this.insertAdjacentHTML('beforebegin', dropHTML); this.insertAdjacentHTML('beforebegin', dropHTML);
let dropElem = this.previousSibling; let dropElem = this.previousSibling;
addDnDHandlers(dropElem); addDnDHandlers(dropElem);
} }

View File

@ -0,0 +1,18 @@
.chart-container {
width: 100%;
height: 40vh;
}
@media only screen and (max-width: 900px) {
.chart-container {
width: 100%;
height: 50vh;
}
}
@media only screen and (orientation: landscape) and (max-width: 900px) {
.chart-container {
width: 100%;
height: 100vh;
}
}

View File

@ -0,0 +1,27 @@
.card-grid {
grid-template-columns: repeat(auto-fit, minmax(325px, 1fr));
grid-row-gap: 0;
grid-column-gap: 8px;
}
@media only screen and (min-width: 470px) {
.card-grid {
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
grid-row-gap: 0;
grid-column-gap: 8px;
}
}
.card-grid-2 {
grid-template-columns: repeat(1, minmax(325px, 1fr));
grid-row-gap: 0;
grid-column-gap: 8px;
}
@media only screen and (min-width: 950px) {
.card-grid-2 {
grid-template-columns: repeat(2, minmax(450px, 1fr));
grid-row-gap: 0;
grid-column-gap: 8px;
}
}

View File

@ -24,9 +24,10 @@
{% endmacro %} {% endmacro %}
{# Simple driver select for forms #} {# Simple driver select for forms #}
{% macro driver_select(name, label, include_none, drivers=none) %} {% macro driver_select(name, label, include_none, drivers=none, disabled=false, border="") %}
<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 {{ border }}" aria-label="{{ name }}"
{% if disabled %}disabled="disabled"{% endif %}>
<option value="" selected disabled hidden></option> <option value="" selected disabled hidden></option>
{% if drivers == none %} {% if drivers == none %}
@ -34,7 +35,7 @@
{% endif %} {% endif %}
{% for driver in drivers %} {% for driver in drivers %}
<option value="{{ driver.name }}">{{ driver.abbr }}</option> <option value="{{ driver.id }}">{{ driver.abbr }}</option>
{% endfor %} {% endfor %}
</select> </select>
<label for="{{ name }}" class="text-primary">{{ label }}</label> <label for="{{ name }}" class="text-primary">{{ label }}</label>
@ -42,9 +43,10 @@
{% endmacro %} {% endmacro %}
{# Driver select for forms where a value might be preselected #} {# Driver select for forms where a value might be preselected #}
{% macro driver_select_with_preselect(driver_match, name, label, include_none, drivers=none) %} {% macro driver_select_with_preselect(driver_match, name, label, include_none, drivers=none, disabled=false, border="") %}
<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 {{ border }}" aria-label="{{ name }}"
{% if disabled %}disabled="disabled"{% endif %}>
{# Use namespace wrapper to persist scope between loop iterations #} {# Use namespace wrapper to persist scope between loop iterations #}
{% set user_has_chosen = namespace(driverpre=false) %} {% set user_has_chosen = namespace(driverpre=false) %}
@ -55,13 +57,13 @@
{% for driver in drivers %} {% for driver in drivers %}
{% if driver_match == driver %} {% 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.id }}">{{ driver.abbr }}</option>
{% else %} {% else %}
<option value="{{ driver.name }}">{{ driver.abbr }}</option> <option value="{{ driver.id }}">{{ driver.abbr }}</option>
{% endif %} {% endif %}
{% if (include_none == true) and (driver == model.none_driver()) %} {% if (include_none == true) and (driver == model.none_driver()) %}
<option disabled>──────────</option> <option disabled="disabled">──────────</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
@ -75,9 +77,10 @@
{% endmacro %} {% endmacro %}
{# Simple team select for forms #} {# Simple team select for forms #}
{% macro team_select(name, label, include_none, teams=none) %} {% macro team_select(name, label, include_none, teams=none, disabled=false, border="") %}
<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 {{ border }}" aria-label="{{ name }}"
{% if disabled %}disabled="disabled"{% endif %}>
<option value="" selected disabled hidden></option> <option value="" selected disabled hidden></option>
{% if teams == none %} {% if teams == none %}
@ -85,7 +88,7 @@
{% endif %} {% endif %}
{% for team in teams %} {% for team in teams %}
<option value="{{ team.name }}">{{ team.name }}</option> <option value="{{ team.id }}">{{ team.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<label for="{{ name }}" class="text-primary">{{ label }}</label> <label for="{{ name }}" class="text-primary">{{ label }}</label>
@ -93,9 +96,10 @@
{% endmacro %} {% endmacro %}
{# Team select for forms where a value might be preselected #} {# Team select for forms where a value might be preselected #}
{% macro team_select_with_preselect(team_match, name, label, include_none, teams=none) %} {% macro team_select_with_preselect(team_match, name, label, include_none, teams=none, disabled=false, border="") %}
<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 {{ border }}" aria-label="{{ name }}"
{% if disabled %}disabled="disabled"{% endif %}>
{# Use namespace wrapper to persist scope between loop iterations #} {# Use namespace wrapper to persist scope between loop iterations #}
{% set user_has_chosen = namespace(teampre=false) %} {% set user_has_chosen = namespace(teampre=false) %}
@ -106,13 +110,13 @@
{% for team in teams %} {% for team in teams %}
{% if team_match == team %} {% if team_match == team %}
{% set user_has_chosen.teampre = true %} {% set user_has_chosen.teampre = true %}
<option selected="selected" value="{{ team.name }}">{{ team.name }}</option> <option selected="selected" value="{{ team.id }}">{{ team.name }}</option>
{% else %} {% else %}
<option value="{{ team.name }}">{{ team.name }}</option> <option value="{{ team.id }}">{{ team.name }}</option>
{% endif %} {% endif %}
{% if (include_none == true) and (team == model.none_team()) %} {% if (include_none == true) and (team == model.none_team()) %}
<option disabled>──────────</option> <option disabled="disabled">──────────</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
@ -176,6 +180,12 @@
<link href="../static/style/bootstrap.css" rel="stylesheet"> <link href="../static/style/bootstrap.css" rel="stylesheet">
<script src="../static/script/bootstrap.bundle.js"></script> <script src="../static/script/bootstrap.bundle.js"></script>
<!-- ChartJS -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Custom -->
<link href="../static/style/grid.css" rel="stylesheet">
<link href="../static/style/diagram.css" rel="stylesheet">
<script defer> <script defer>
{# Initialize Bootstrap Tooltips #} {# Initialize Bootstrap Tooltips #}
let tooltipTriggerList = document.querySelectorAll("[data-bs-toggle='tooltip']") let tooltipTriggerList = document.querySelectorAll("[data-bs-toggle='tooltip']")
@ -189,7 +199,7 @@
<nav class="navbar fixed-top navbar-expand-lg bg-body-tertiary shadow-sm"> <nav class="navbar fixed-top navbar-expand-lg bg-body-tertiary shadow-sm">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/race"> <a class="navbar-brand" href="/race/Everyone">
<img src="../static/image/f1_logo.svg" alt="Logo" width="120" height="30" <img src="../static/image/f1_logo.svg" alt="Logo" width="120" height="30"
class="d-inline-block align-text-top"> class="d-inline-block align-text-top">
Formula 10 Formula 10
@ -203,7 +213,8 @@
<div class="navbar-nav me-2"> <div class="navbar-nav me-2">
{{ nav_selector(page="/race/" ~ model.active_user_name_sanitized_or_everyone(), text="Race Picks") }} {{ nav_selector(page="/race/" ~ model.active_user_name_sanitized_or_everyone(), text="Race Picks") }}
{{ nav_selector(page="/season/" ~ model.active_user_name_sanitized_or_everyone(), text="Season Picks") }} {{ nav_selector(page="/season/" ~ model.active_user_name_sanitized_or_everyone(), text="Season Picks") }}
{{ nav_selector(page="/graphs", text="Statistics") }} {{ nav_selector(page="/graphs", text="Leaderboard") }}
{{ nav_selector(page="/stats", text="Statistics") }}
{{ nav_selector(page="/rules", text="Rules") }} {{ nav_selector(page="/rules", text="Rules") }}
</div> </div>
@ -211,7 +222,7 @@
<div class="flex-grow-1"></div> <div class="flex-grow-1"></div>
<div class="navbar-nav"> <div class="navbar-nav">
{{ nav_selector(page="/result", text="Enter Race Result") }} {# {{ nav_selector(page="/result", text="Enter Race Result") }} #}
{{ nav_selector(page="/user", text="Manage Users") }} {{ nav_selector(page="/user", text="Manage Users") }}
</div> </div>
</div> </div>

View File

@ -1,104 +0,0 @@
{% extends 'base.jinja' %}
{% block title %}Formula 10 - Race Result{% endblock title %}
{% set active_page = "/result" %}
{% block head_extra %}
<link href="../static/style/draggable.css" rel="stylesheet">
<script src="../static/script/draggable.js" defer></script>
{% endblock head_extra %}
{% block navbar_center %}
{% if model.all_race_results() | length > 0 %}
<div class="dropdown">
<button class="btn btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
{{ model.active_result_race_name_or_current_race_name() }}
</button>
<ul class="dropdown-menu">
{% if model.current_race is not none %}
<li>
<a class="dropdown-item"
href="/result/{{ model.current_race.name_sanitized }}">{{ model.current_race.name }}</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
{% endif %}
{% for result in model.all_race_results() %}
<li>
<a class="dropdown-item"
href="/result/{{ result.race.name_sanitized }}">{{ result.race.name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock navbar_center %}
{% block body %}
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));">
<div class="card shadow-sm" style="width: 450px;">
<div class="card-body">
<h5 class="card-title">
{{ model.active_result_race_name_or_current_race_name() }}
</h5>
<form action="/result-enter/{{ model.active_result_race_name_or_current_race_name_sanitized() }}"
method="post">
<ul id="columns" class="list-group list-group-flush">
{% for driver in model.all_drivers_or_active_result_standing_drivers() %}
<li class="list-group-item column p-1" draggable="true">
{{ driver.name }}
<div class="d-inline-block float-end">
{# Driver DNFed at first #}
<div class="form-check form-check-reverse d-inline-block">
<input type="checkbox" class="form-check-input" value="{{ driver.name }}"
id="first-dnf-{{ driver.name }}" name="first-dnf-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.initial_dnf) %}checked{% endif %}>
<label for="first-dnf-{{ driver.name }}"
class="form-check-label text-muted">1. DNF</label>
</div>
{# Driver DNFed #}
<div class="form-check form-check-reverse d-inline-block mx-2">
<input type="checkbox" class="form-check-input" value="{{ driver.name }}"
id="dnf-{{ driver.name }}" name="dnf-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.all_dnfs) %}checked{% endif %}>
<label for="dnf-{{ driver.name }}"
class="form-check-label text-muted">DNF</label>
</div>
{# Driver Excluded #}
<div class="form-check form-check-reverse d-inline-block">
<input type="checkbox" class="form-check-input" value="{{ driver.name }}"
id="exclude-{{ driver.name }}" name="excluded-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.standing_exclusions) %}checked{% endif %}>
<label for="exclude-{{ driver.name }}"
class="form-check-label text-muted" data-bs-toggle="tooltip"
title="Driver is not counted for standing">NC</label>
</div>
</div>
{# Standing order #}
<input type="hidden" name="pxx-drivers" value="{{ driver.name }}"></li>
{% endfor %}
</ul>
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save">
</form>
</div>
</div>
</div>
{% endblock body %}

View File

@ -0,0 +1,16 @@
{% extends 'base.jinja' %}
{% block title %}Formula 10 - Error{% endblock title %}
{% set active_page = "/error" %}
{% block body %}
<div class="card shadow-sm mb-2">
<div class="card-header">
<span class="text-danger fw-bold">Error</span>
</div>
<div class="card-body">
{{ error_message }}
</div>
</div>
{% endblock body %}

View File

@ -0,0 +1,106 @@
{% extends 'base.jinja' %}
{% block title %}Formula 10 - Leaderboard{% endblock title %}
{% set active_page = "/graphs" %}
{% block body %}
<div class="card shadow-sm mb-2">
<div class="card-header">
Note
</div>
<div class="card-body">
Points only include race picks.
</div>
</div>
<div class="card shadow-sm mb-2">
<div class="card-header">
Leaderboard
</div>
<div class="card-body">
<div class="d-inline-block overflow-x-scroll w-100">
<table class="table table-bordered table-sm table-responsive">
<thead>
<tr>
<th scope="col" class="text-center" style="min-width: 50px;">Place</th>
<th scope="col" class="text-center" style="min-width: 50px;">User</th>
<th scope="col" class="text-center" style="min-width: 100px;">Points</th>
<th scope="col" class="text-center" style="min-width: 100px;">Total picks</th>
<th scope="col" class="text-center" style="min-width: 100px;" data-bs-toggle="tooltip"
title="Any points count as correct">Correct picks
</th>
<th scope="col" class="text-center" style="min-width: 100px;">Points per pick</th>
</tr>
</thead>
<tbody>
{% for user in points.users_sorted_by_points() %}
{% set user_standing = points.user_standing()[user.name] %}
<tr class="{% if user_standing == 1 %}table-danger{% endif %}">
<td class="text-center text-nowrap">{{ user_standing }}</td>
<td class="text-center text-nowrap">{{ user.name }}</td>
<td class="text-center text-nowrap">{{ points.total_points_by(user.name) }}</td>
<td class="text-center text-nowrap">{{ points.picks_count(user.name) }}</td>
<td class="text-center text-nowrap">{{ points.picks_with_points_count(user.name) }}</td>
<td class="text-center text-nowrap">{{ "%0.2f" % points.points_per_pick(user.name) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="card shadow-sm mb-2">
<div class="card-header">
History
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="line-chart"></canvas>
</div>
<script>
function cumulative_points(data) {
return new Chart(document.getElementById("line-chart"), {
type: 'line',
data: data,
options: {
title: {
display: true,
text: 'History'
},
{#tension: 0,#}
responsive: true,
maintainAspectRatio: false,
pointRadius: 5,
pointHoverRadius: 10,
scales: {
y: {
min: 0,
{#max: 100#}
}
}
}
});
}
cumulative_points({{ points.cumulative_points_data() | safe }})
</script>
</div>
</div>
{# <div class="card mt-2">#}
{# <div class="card-body">#}
{# <h5 class="card-title">Statistics</h5>#}
{# Various statistics: Driver voted most for DNF #}
{# </div>#}
{# </div>#}
{% endblock body %}

View File

@ -4,167 +4,201 @@
{% set active_page = "/race/" ~ model.active_user_name_sanitized_or_everyone() %} {% set active_page = "/race/" ~ model.active_user_name_sanitized_or_everyone() %}
{% block head_extra %}
<script src="../static/script/countdown.js" defer></script>
{% endblock head_extra %}
{% block navbar_center %} {% block navbar_center %}
{{ active_user_dropdown(page='race') }} {{ active_user_dropdown(page='race') }}
{% endblock navbar_center %} {% endblock navbar_center %}
{% block body %} {% block body %}
<table class="table table-bordered table-sm table-responsive shadow-sm"> {# Put table in this div to make right padding work #}
<thead> <div class="d-inline-block overflow-x-scroll w-100 mb-2">
<tr> <table class="table table-bordered table-sm table-responsive shadow-sm">
<th scope="col" rowspan="2" class="text-center" style="width: 200px;">Race</th>
<th scope="col" {% if model.active_user is none %}colspan="{{ model.all_users() | length }}"{% endif %} <thead>
class="text-center">Call <tr>
</th> <th scope="col" rowspan="2" class="text-center" style="width: 125px;">Race</th>
<th scope="col" rowspan="2" class="text-center" style="width: 200px;">Result</th> <th scope="col" {% if model.active_user is none %}colspan="{{ model.all_users() | length }}"{% endif %}
</tr> class="text-center">Call
</thead> </th>
<tbody> <th scope="col" rowspan="2" class="text-center" style="width: 125px;">Result</th>
</tr>
</thead>
{# Users List #} <tbody>
<tr>
<td>&nbsp;</td>
{# Link should only be visible if all users are visible #} {# Users List #}
{% if model.active_user is not none %} <tr>
<td class="text-center text-nowrap" style="min-width: 100px;">{{ model.active_user.name }}</td> <td>&nbsp;</td>
{% else %}
{% for user in model.all_users() %} {# Link should only be visible if all users are visible #}
<td class="text-center text-nowrap" style="min-width: 100px;"> {% if model.active_user is not none %}
<a href="/race/{{ user.name_sanitized }}" class="link-dark">{{ user.name }}</a> <td class="text-center text-nowrap" style="min-width: 100px;">{{ model.active_user.name }}
({{ points.total_points_by(model.active_user.name) }})
</td> </td>
{% endfor %} {% else %}
{% endif %}
<td>&nbsp;</td>
</tr>
{# Current Result, only displayed for all users overview and if guess is remaining #}
{% if (model.active_user is none) and (model.current_race is not none) %}
<tr class="table-danger">
<td class="text-nowrap">
<span class="fw-bold">{{ model.current_race.number }}:</span> {{ model.current_race.name }}<br>
<small><span class="fw-bold">Guess:</span> P{{ model.current_race.place_to_guess }}</small>
</td>
{% if model.all_users() | length > 0 %}
{% 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=model.current_race.name) %} <td class="text-center text-nowrap" style="min-width: 100px;">
<a href="/race/{{ user.name_sanitized }}" class="link-dark">{{ user.name }}
<td class="text-center text-nowrap"> ({{ points.total_points_by(user.name) }})</a>
{% if user_guess is not none %}
<ul class="list-group list-group-flush">
<li class="list-group-item" style="background-color: inherit;">
{{ user_guess.pxx_guess.abbr }}
</li>
<li class="list-group-item" style="background-color: inherit;">
{{ user_guess.dnf_guess.abbr }}
</li>
</ul>
{% else %}
&nbsp;
{% endif %}
</td> </td>
{% endfor %} {% endfor %}
{% else %}
<td>&nbsp;</td>
{% endif %} {% endif %}
<td>&nbsp;</td> <td>&nbsp;</td>
</tr> </tr>
{% endif %}
{# Enter Guess, only displayed for single user focused view and if guess is remaining #} {# Current Result, only displayed for all users overview and if guess is remaining #}
{% if (model.active_user is not none) and (model.current_race is not none) %} {% if (model.active_user is none) and (model.current_race is not none) %}
<tr class="table-danger">
<td class="text-nowrap">
<span class="fw-bold">{{ model.current_race.number }}:</span> {{ model.current_race.name }}<br>
<small><span class="fw-bold">Guess:</span> P{{ model.current_race.place_to_guess }}</small>
</td>
<td> <tr class="table-danger">
<form action="/race-guess/{{ model.current_race.name_sanitized }}/{{ model.active_user.name_sanitized }}" <td class="text-nowrap">
method="post"> <span class="fw-bold">{{ model.current_race.number }}:</span> {{ model.current_race.name }}<br>
{% set user_guess = model.race_guesses_by(user_name=model.active_user.name, race_name=model.current_race.name) %} <small><span class="fw-bold">Guess:</span> P{{ model.current_race.place_to_guess }}</small><br>
<small><span class="fw-bold">Date:</span> {{ model.current_race.date.strftime("%d.%m %H:%M") }}
</small>
</td>
{# Driver PXX Select #} {% if model.all_users() | length > 0 %}
{{ driver_select_with_preselect(driver_match=user_guess.pxx_guess, name="pxxselect", label="P" ~ model.current_race.place_to_guess ~ ":", include_none=true) }} {% for user in model.all_users() %}
{% set user_guess = model.race_guesses_by(user_name=user.name, race_name=model.current_race.name) %}
<div class="mt-2"></div> <td class="text-center text-nowrap">
{% if user_guess is not none %}
<ul class="list-group list-group-flush">
<li class="list-group-item" style="background-color: inherit;">
{{ user_guess.pxx_guess.abbr }}
</li>
<li class="list-group-item" style="background-color: inherit;">
{{ user_guess.dnf_guess.abbr }}
</li>
</ul>
{% else %}
&nbsp;
{% endif %}
</td>
{% endfor %}
{% else %}
<td>&nbsp;</td>
{% endif %}
{# Driver DNF Select #} {# Race countdown #}
{{ driver_select_with_preselect(driver_match=user_guess.dnf_guess, name="dnfselect", label="DNF:", include_none=true) }} <span id="race_date" hidden="hidden">{{ model.current_race.date.strftime("%Y-%m-%dT%H:%M") }}</span>
<td class="text-center text-nowrap align-middle">
<span class="fw-bold">Race starts in:</span><br>
<span id="race_date_countdown">00d 00h 00m 00s</span>
</td>
</tr>
{% endif %}
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save"> {# Enter Guess, only displayed for single user focused view and if guess is remaining #}
</form> {% if (model.active_user is not none) and (model.current_race is not none) %}
<form action="/race-guess-delete/{{ model.current_race.name_sanitized }}/{{ model.active_user.name_sanitized }}" <tr class="table-danger">
method="post"> <td class="text-nowrap">
<input type="submit" class="btn btn-dark mt-2 w-100" value="Delete"> <span class="fw-bold">{{ model.current_race.number }}:</span> {{ model.current_race.name }}<br>
</form> <small><span class="fw-bold">Guess:</span> P{{ model.current_race.place_to_guess }}</small><br>
</td> <small><span class="fw-bold">Date:</span> {{ model.current_race.date.strftime("%d.%m %H:%M") }}
</small>
</td>
<td>&nbsp;</td> <td>
</tr> {% set race_guess_open = model.race_guess_open(model.current_race) %}
{% endif %} {% if race_guess_open == true %}
{% set action_save_href = "/race-guess/" ~ model.current_race.name_sanitized ~ "/" ~ model.active_user.name_sanitized %}
{% set action_delete_href = "/race-guess-delete/" ~ model.current_race.name_sanitized ~ "/" ~ model.active_user.name_sanitized %}
{% else %}
{% set action_save_href = "" %}
{% set action_delete_href = "" %}
{% endif %}
{# Past Race Results #} {# Enter + Save guess #}
{% for past_result in model.all_race_results() %} <form action="{{ action_save_href }}" method="post">
<tr> {% set user_guess = model.race_guesses_by(user_name=model.active_user.name, race_name=model.current_race.name) %}
<td class="text-nowrap">
<span class="fw-bold">{{ past_result.race.number }}:</span> {{ past_result.race.name }}<br>
<small><span class="fw-bold">Guessed:</span> P{{ past_result.race.place_to_guess }}</small>
</td>
{% if model.all_users_or_active_user() | length > 0 %} {# Driver PXX Select #}
{% for user in model.all_users_or_active_user() %} {{ driver_select_with_preselect(driver_match=user_guess.pxx_guess, name="pxxselect", label="P" ~ model.current_race.place_to_guess ~ ":", include_none=true, disabled=not race_guess_open) }}
<td class="text-center text-nowrap">
{% set user_guess = model.race_guesses_by(user_name=user.name, race_name=past_result.race.name) %}
{% if user_guess is not none %} <div class="mt-2"></div>
<ul class="list-group list-group-flush">
<li class="list-group-item {{ pxx_guess_colorization(guessed_driver=user_guess.pxx_guess, result=past_result) }}"> {# Driver DNF Select #}
{{ driver_select_with_preselect(driver_match=user_guess.dnf_guess, name="dnfselect", label="DNF:", include_none=true, disabled=not race_guess_open) }}
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save"
{% if race_guess_open == false %}disabled="disabled"{% endif %}>
</form>
{# Delete guess #}
<form action="{{ action_delete_href }}" method="post">
<input type="submit" class="btn btn-dark mt-2 w-100" value="Delete"
{% if race_guess_open == false %}disabled="disabled"{% endif %}>
</form>
</td>
<td>&nbsp;</td>
</tr>
{% endif %}
{# Past Race Results #}
{% for past_result in model.all_race_results() %}
<tr>
<td class="text-nowrap">
<span class="fw-bold">{{ past_result.race.number }}:</span> {{ past_result.race.name }}<br>
<small><span class="fw-bold">Guessed:</span> P{{ past_result.race.place_to_guess }}</small><br>
<small><span class="fw-bold">Date:</span> {{ past_result.race.date.strftime("%d.%m %H:%M") }}
</small>
</td>
{% if model.all_users_or_active_user() | length > 0 %}
{% for user in model.all_users_or_active_user() %}
<td class="text-center text-nowrap">
{% set user_guess = model.race_guesses_by(user_name=user.name, race_name=past_result.race.name) %}
{% if user_guess is not none %}
<ul class="list-group list-group-flush">
<li class="list-group-item {{ pxx_guess_colorization(guessed_driver=user_guess.pxx_guess, result=past_result) }}">
<span data-bs-toggle="tooltip" <span data-bs-toggle="tooltip"
title="{{ past_result.driver_standing_points_string(user_guess.pxx_guess) }}"> 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) }} {{ user_guess.pxx_guess.abbr ~ past_result.driver_standing_position_string(user_guess.pxx_guess) }}
</span> </span>
</li> </li>
<li class="list-group-item {{ dnf_guess_colorization(guessed_driver=user_guess.dnf_guess, result=past_result) }}"> <li class="list-group-item {{ dnf_guess_colorization(guessed_driver=user_guess.dnf_guess, result=past_result) }}">
<span data-bs-toggle="tooltip" <span data-bs-toggle="tooltip"
title="{{ past_result.driver_dnf_points_string(user_guess.dnf_guess) }}"> title="{{ past_result.driver_dnf_points_string(user_guess.dnf_guess) }}">
{{ user_guess.dnf_guess.abbr }} {{ user_guess.dnf_guess.abbr }}
</span> </span>
</li> </li>
</ul> </ul>
{% else %} {% else %}
&nbsp; &nbsp;
{% endif %} {% endif %}
</td> </td>
{% endfor %} {% endfor %}
{% else %} {% else %}
<td>&nbsp;</td> <td>&nbsp;</td>
{% endif %} {% endif %}
{# Actual result #} {# 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(result=past_result) }}"> <span data-bs-toggle="tooltip" title="{{ pxx_standing_tooltip_text(result=past_result) }}">
P{{ past_result.race.place_to_guess }}: {{ past_result.offset_from_place_to_guess(0).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: {{ past_result.initial_dnf_string() }}</li> DNF: {{ past_result.initial_dnf_string() }}</li>
</ul> </ul>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
{% endblock body %} {% endblock body %}

View File

@ -0,0 +1,131 @@
{% extends 'base.jinja' %}
{% block title %}Formula 10 - Race Result{% endblock title %}
{% set active_page = "/result" %}
{% block head_extra %}
<link href="../static/style/draggable.css" rel="stylesheet">
<script src="../static/script/draggable.js" defer></script>
{% endblock head_extra %}
{% block navbar_center %}
{% if model.all_race_results() | length > 0 %}
<div class="dropdown">
<button class="btn btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
{{ model.active_result_race_name_or_current_race_name() }}
</button>
<ul class="dropdown-menu">
{% if model.current_race is not none %}
<li>
<a class="dropdown-item"
href="/result/{{ model.current_race.name_sanitized }}">{{ model.current_race.name }}</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
{% endif %}
{% for result in model.all_race_results() %}
<li>
<a class="dropdown-item"
href="/result/{{ result.race.name_sanitized }}">{{ result.race.name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock navbar_center %}
{% block body %}
<div class="grid card-grid">
<div class="card shadow-sm mb-2" style="max-width: 450px;">
<div class="card-header">
{{ model.active_result_race_name_or_current_race_name() }}
</div>
<div class="card-body">
<div class="d-inline-block overflow-x-scroll w-100">
<div style="width: 410px;">
{% set race_result_open=model.race_result_open(model.active_result_race_name_or_current_race_name()) %}
{% if race_result_open == true %}
{% set action_save_href = "/result-enter/" ~ model.active_result_race_name_or_current_race_name_sanitized() %}
{% else %}
{% set action_save_href = "" %}
{% endif %}
<form action="{{ action_save_href }}" method="post">
{# Place numbers #}
<ul class="list-group list-group-flush d-inline-block">
{% for driver in model.all_drivers_or_active_result_standing_drivers() %}
<li class="list-group-item p-1"><span id="place_number"
class="fw-bold">P{{ "%02d" % loop.index }}</span>:
</li>
{% endfor %}
</ul>
{# Drag and drop #}
<ul id="columns" class="list-group list-group-flush d-inline-block float-end">
{% for driver in model.all_drivers_or_active_result_standing_drivers() %}
<li class="list-group-item {% if race_result_open == true %}column{% endif %} p-1"
{% if race_result_open == true %}draggable="true"{% endif %}>
{{ driver.name }}
<div class="d-inline-block float-end" style="margin-left: 30px;">
{# Driver DNFed at first #}
<div class="form-check form-check-reverse d-inline-block">
<input type="checkbox" class="form-check-input"
value="{{ driver.id }}"
id="first-dnf-{{ driver.id }}" name="first-dnf-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.initial_dnf) %}checked{% endif %}
{% if race_result_open == false %}disabled="disabled"{% endif %}>
<label for="first-dnf-{{ driver.id }}"
class="form-check-label text-muted">1. DNF</label>
</div>
{# Driver DNFed #}
<div class="form-check form-check-reverse d-inline-block mx-2">
<input type="checkbox" class="form-check-input"
value="{{ driver.id }}"
id="dnf-{{ driver.id }}" name="dnf-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.all_dnfs) %}checked{% endif %}
{% if race_result_open == false %}disabled="disabled"{% endif %}>
<label for="dnf-{{ driver.id }}"
class="form-check-label text-muted">DNF</label>
</div>
{# Driver Excluded #}
<div class="form-check form-check-reverse d-inline-block">
<input type="checkbox" class="form-check-input"
value="{{ driver.id }}"
id="exclude-{{ driver.id }}" name="excluded-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.standing_exclusions) %}checked{% endif %}
{% if race_result_open == false %}disabled="disabled"{% endif %}>
<label for="exclude-{{ driver.id }}"
class="form-check-label text-muted" data-bs-toggle="tooltip"
title="Driver is not counted for standing">NC</label>
</div>
</div>
{# Standing order #}
<input type="hidden" name="pxx-drivers" value="{{ driver.id }}">
</li>
{% endfor %}
</ul>
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save"
{% if race_result_open == false %}disabled="disabled"{% endif %}>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock body %}

View File

@ -0,0 +1,96 @@
{% extends 'base.jinja' %}
{% block title %}Formula 10 - Rules{% endblock title %}
{% set active_page = "/rules" %}
{% block body %}
<div class="card shadow-sm mb-2">
<div class="card-header">
Format
</div>
<div class="card-body">
<p>
Das Tippspiel wird dieses Jahr in zwei Teile aufgespalten: Renntips (für jedes Rennen) und Bonustipps
(für die ganze Saison).
Renntipps müssen bis Rennstart, Bonustipps bis 1. März abgegeben werden.
Tipps können entweder selbst eingetragen werden, oder in die Gruppe geschrieben werden, um gültig zu
sein.
</p>
</div>
</div>
<div class="card shadow-sm mb-2">
<div class="card-header">
Renntipps
</div>
<div class="card-body">
<p>
Jedes Rennen besteht aus zwei Tipps.
Einem Platzierungstipp (PX-Tipp) und einem Tipp welcher Fahrer als Erstes das Rennen abbricht
(DNF-Tipp).
Anders als beim P10-Tipp letztes Jahr wird dieses Mal jedes Rennen eine andere Platzierung getippt.
Die zu tippende Platzierung wird auf der Tabellen-Übersichtsseite und im Fahrer-Auswahlmenü angezeigt.
</p>
<p>
DNS/DSQ zählt hierbei nicht als DNF, außerdem besteht die Möglichkeit zu tippen, dass kein Fahrer DNF't oder
das Rennen auf PX beendet.
</p>
</div>
</div>
<div class="card shadow-sm mb-2">
<div class="card-header">
Punkte
</div>
<div class="card-body">
<p>
Für einen korrekten Tipp werden 10 Punkte vergeben.
Beim PX-Tipp werden 6 Punkte für einen Platz Abweichung, 3 Punkte für zwei plätze Abweichung und 1 Punkt
für 3 Plätze Abweichung vergeben.
</p>
<p>
Beim DNF-Tipp werden keine Abweichungspunkte vergeben, ebensowenig wie bei der Auswahl "Keiner" für den
PX-Tipp.
</p>
</div>
</div>
<div class="card shadow-sm mb-2">
<div class="card-header">
Bonustipps
</div>
<div class="card-body">
<p>
Zusätzlich gibt es dieses Jahr auch Tipps, die sich auf die ganze Saison beziehen und verschieden
bepunktet werden.
</p>
<ul class="list-group list-group-numbered">
<li class="list-group-item">Welches Team wird P2 in der constructors championship? (10 Punkte für die
richtige Antwort / 0 Punkte für eine falsche Antwort)
</li>
<li class="list-group-item">Welcher Fahrer verbessert am meisten seine Platzierung in der drivers
championship im Vergleich zum Vorjahr? (10/0)
</li>
<li class="list-group-item">Welcher Fahrer verschlechtert am meisten seine Platzierung in der drivers
championship im Vergleich zum Vorjahr? (10/0)
</li>
<li class="list-group-item">Welcher Fahrer hat die meisten DNFs? (10/0)</li>
<li class="list-group-item">Welcher Fahrer hat die meisten Überholungen? (10/0)</li>
<li class="list-group-item">Nenne alle Fahrer, die mindestens einmal auf dem Podium stehen werden.
(3/-2, nicht-gegebene Antworten zählen als falsch)
</li>
<li class="list-group-item">Nenne für alle Teams den Sieger des teaminternen Duells. (3/-3)</li>
<li class="list-group-item">Nenne einen hot-take für die Saison (10/0, "hotness" wird vorher abgesegnet)</li>
</ul>
</div>
</div>
{% endblock body %}

View File

@ -10,62 +10,91 @@
{% block body %} {% block body %}
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));"> <div class="card shadow-sm mb-2">
<div class="card-header">
Note
</div>
<div class="card-body">
Picks that match the current standings are marked in green, except for the hot-take and overtake picks, as
those are not evaluated automatically.<br>
Points from sprints and fastest laps are not tracked currently.
</div>
</div>
<div class="grid card-grid">
{% for user in model.all_users_or_active_user() %} {% for user in model.all_users_or_active_user() %}
<div class="card mb-2 shadow-sm" style="width: 450px;"> <div class="card shadow-sm mb-2">
<div class="card-body"> <div class="card-header">
{# Link should only be visible if all users are visible #} {# Link should only be visible if all users are visible #}
{% if model.active_user is not none %} {% if model.active_user is not none %}
<h5 class="card-title">{{ user.name }}</h5> {{ user.name }}
{% else %} {% else %}
<a href="/season/{{ user.name }}" class="link-dark"> <a href="/season/{{ user.name }}" class="link-dark">{{ user.name }}</a>
<h5 class="card-title">{{ user.name }}</h5>
</a>
{% endif %} {% endif %}
</div>
<div class="card-body">
{% set user_guess = model.season_guesses_by(user_name=user.name) %} {% set user_guess = model.season_guesses_by(user_name=user.name) %}
<form action="/season-guess/{{ user.name }}" method="post"> {% set season_guess_open = model.season_guess_open() %}
{% if season_guess_open == true %}
{% set action_save_href = "/season-guess/" ~ user.name %}
{% else %}
{% set action_save_href = "" %}
{% endif %}
<form action="{{ action_save_href }}" method="post">
{# Hot Take #} {# Hot Take #}
<div class="form-floating"> <div class="form-floating">
{% if user_guess is not none %} <textarea
<textarea class="form-control" id="hot-take-input-{{ user.name }}" name="hottakeselect" class="form-control {% if points.hot_take_correct(user.name) %}border-success{% endif %}"
style="height: 150px">{{ user_guess.hot_take_string() }}</textarea> id="hot-take-input-{{ user.name }}" name="hottakeselect"
{% else %} style="height: 150px"
<textarea class="form-control" id="hot-take-input-{{ user.name }}" name="hottakeselect" {% if season_guess_open == false %}disabled="disabled"{% endif %}>
style="height: 150px"></textarea> {%- if user_guess is not none -%}{{ user_guess.hot_take_string() }}{%- endif -%}
{% endif %} </textarea>
<label for="hot-take-input-{{ user.name }}" class="text-primary">Hot Take:</label> <label for="hot-take-input-{{ user.name }}" class="text-primary">Hot Take:</label>
</div> </div>
{# P2 Constructor #} {# P2 Constructor #}
<div class="mt-2"> <div class="mt-2">
{{ team_select_with_preselect(team_match=user_guess.p2_wcc, name="p2select", label="P2 in WCC:", include_none=false) }} {{ team_select_with_preselect(team_match=user_guess.p2_wcc, name="p2select", label="P2 in WCC:",
include_none=false, disabled=not season_guess_open,
border=("border-success" if points.p2_constructor_correct(user.name) else "")) }}
</div> </div>
{# Most Overtakes + DNFs #} {# Most Overtakes + DNFs #}
<div class="input-group mt-2"> <div class="input-group mt-2">
{{ driver_select_with_preselect(driver_match=user_guess.most_overtakes, name="overtakeselect", label="Most overtakes:", include_none=false) }} {{ driver_select_with_preselect(driver_match=user_guess.most_overtakes, name="overtakeselect",
{{ driver_select_with_preselect(driver_match=user_guess.most_dnfs, name="dnfselect", label="Most DNFs:", include_none=false) }} label="Most overtakes:", include_none=false, disabled=not season_guess_open,
border=("border-success" if points.overtakes_correct(user.name) else "")) }}
{{ driver_select_with_preselect(driver_match=user_guess.most_dnfs, name="dnfselect", label="Most DNFs:",
include_none=false, disabled=not season_guess_open,
border=("border-success" if points.dnfs_correct(user.name) else "")) }}
</div> </div>
{# Most Gained + Lost #} {# Most Gained + Lost #}
<div class="input-group mt-2" data-bs-toggle="tooltip" <div class="input-group mt-2" data-bs-toggle="tooltip"
title="Which driver will gain/lose the most places in comparison to last season's results?"> title="Which driver will gain/lose the most places in comparison to last season's results?">
{{ driver_select_with_preselect(driver_match=user_guess.most_wdc_gained, name="gainedselect", label="Most WDC places gained:", include_none=false, drivers=model.drivers_for_wdc_gained()) }} {{ driver_select_with_preselect(driver_match=user_guess.most_wdc_gained, name="gainedselect",
{{ driver_select_with_preselect(driver_match=user_guess.most_wdc_lost, name="lostselect", label="Most WDC places lost:", include_none=false) }} label="Most WDC pl. gained:", include_none=false, drivers=model.drivers_for_wdc_gained(),
disabled=not season_guess_open,
border=("border-success" if points.most_gained_correct(user.name) else "")) }}
{{ driver_select_with_preselect(driver_match=user_guess.most_wdc_lost, name="lostselect",
label="Most WDC pl. lost:", include_none=false, disabled=not season_guess_open,
border=("border-success" if points.most_lost_correct(user.name) else "")) }}
</div> </div>
{# Team-internal Winners #} {# Team-internal Winners #}
<h6 class="card-subtitle mt-2" data-bs-toggle="tooltip" <h6 class="card-subtitle mt-2" data-bs-toggle="tooltip"
title="Which driver will finish the season higher than his teammate?">Teammate battle title="Which driver will finish the season higher than his teammate?">Teammate battle
winners:</h6> winners:</h6>
<div class="grid mt-2" style="width: 450px; row-gap: 0;"> <div class="grid mt-2 container" style="row-gap: 0;">
{% for team in model.all_teams(include_none=false) %} {% for team in model.all_teams(include_none=false) %}
{% set driver_a = model.drivers_by(team_name=team.name)[0] %} {% set driver_a = model.drivers_by(team_name=team.name)[0] %}
{% set driver_b = model.drivers_by(team_name=team.name)[1] %} {% set driver_b = model.drivers_by(team_name=team.name)[1] %}
@ -73,24 +102,26 @@
<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.id }}"
id="teamwinner-{{ team.name }}-1-{{ user.name }}" id="teamwinner-{{ team.id }}-1-{{ user.id }}"
value="{{ driver_a.name }}" value="{{ driver_a.id }}"
{% if (user_guess is not none) and (driver_a in user_guess.team_winners) %}checked="checked"{% endif %}> {% if (user_guess is not none) and (driver_a in user_guess.team_winners) %}checked="checked"{% endif %}
<label class="form-check-label" {% if season_guess_open == false %}disabled="disabled"{% endif %}>
for="teamwinner-{{ team.name }}-1-{{ user.name }}">{{ driver_a.name }}</label> <label class="form-check-label {% if (user_guess is not none) and (driver_a in user_guess.team_winners) and points.is_team_winner(driver_a) %}text-success{% endif %}"
for="teamwinner-{{ team.id }}-1-{{ user.id }}">{{ driver_a.name }}</label>
</div> </div>
</div> </div>
<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.id }}"
id="teamwinner-{{ team.name }}-2-{{ user.name }}" id="teamwinner-{{ team.id }}-2-{{ user.id }}"
value="{{ driver_b.name }}" value="{{ driver_b.id }}"
{% if (user_guess is not none) and (driver_b in user_guess.team_winners) %}checked="checked"{% endif %}> {% if (user_guess is not none) and (driver_b in user_guess.team_winners) %}checked="checked"{% endif %}
<label class="form-check-label" {% if season_guess_open == false %}disabled="disabled"{% endif %}>
for="teamwinner-{{ team.name }}-2-{{ user.name }}">{{ driver_b.name }}</label> <label class="form-check-label {% if (user_guess is not none) and (driver_b in user_guess.team_winners) and points.is_team_winner(driver_b) %}text-success{% endif %}"
for="teamwinner-{{ team.id }}-2-{{ user.id }}">{{ driver_b.name }}</label>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
@ -99,7 +130,7 @@
{# Drivers with Podiums #} {# Drivers with Podiums #}
<h6 class="card-subtitle mt-2" data-bs-toggle="tooltip" <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> 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 container" style="row-gap: 0;">
{% for team in model.all_teams(include_none=false) %} {% for team in model.all_teams(include_none=false) %}
{% set driver_a = model.drivers_by(team_name=team.name)[0] %} {% set driver_a = model.drivers_by(team_name=team.name)[0] %}
{% set driver_b = model.drivers_by(team_name=team.name)[1] %} {% set driver_b = model.drivers_by(team_name=team.name)[1] %}
@ -108,11 +139,12 @@
<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.id }}-{{ user.id }}"
value="{{ driver_a.name }}" value="{{ driver_a.id }}"
{% if (user_guess is not none) and (driver_a in user_guess.podiums) %}checked="checked"{% endif %}> {% if (user_guess is not none) and (driver_a in user_guess.podiums) %}checked="checked"{% endif %}
<label class="form-check-label" {% if season_guess_open == false %}disabled="disabled"{% endif %}>
for="podium-{{ driver_a.name }}-{{ user.name }}">{{ driver_a.name }}</label> <label class="form-check-label {% if (user_guess is not none) and (driver_a in user_guess.podiums) and points.has_podium(driver_a) %}text-success{% endif %}"
for="podium-{{ driver_a.id }}-{{ user.id }}">{{ driver_a.name }}</label>
</div> </div>
</div> </div>
@ -120,17 +152,19 @@
<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.id }}-{{ user.id }}"
value="{{ driver_b.name }}" value="{{ driver_b.id }}"
{% if (user_guess is not none) and (driver_b in user_guess.podiums) %}checked="checked"{% endif %}> {% if (user_guess is not none) and (driver_b in user_guess.podiums) %}checked="checked"{% endif %}
<label class="form-check-label" {% if season_guess_open == false %}disabled="disabled"{% endif %}>
for="podium-{{ driver_b.name }}-{{ user.name }}">{{ driver_b.name }}</label> <label class="form-check-label {% if (user_guess is not none) and (driver_b in user_guess.podiums) and points.has_podium(driver_b) %}text-success{% endif %}"
for="podium-{{ driver_b.id }}-{{ user.id }}">{{ driver_b.name }}</label>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save"> <input type="submit" class="btn btn-danger mt-2 w-100" value="Save"
{% if season_guess_open == false %}disabled="disabled"{% endif %}>
</form> </form>
</div> </div>

View File

@ -0,0 +1,171 @@
{% extends 'base.jinja' %}
{% block title %}Formula 10 - Statistics{% endblock title %}
{% set active_page = "/stats" %}
{% block body %}
<div class="card shadow-sm mb-2">
<div class="card-header">
Note
</div>
<div class="card-body">
Points from sprints and fastest laps are not tracked currently.
</div>
</div>
<div class="grid card-grid-2">
<div class="card shadow-sm mb-2">
<div class="card-header">
Drivers
</div>
<div class="card-body">
<div class="d-inline-block overflow-x-scroll w-100">
<table class="table table-bordered table-sm table-responsive">
<thead>
<tr>
<th scope="col" class="text-center" style="min-width: 50px;">Place</th>
<th scope="col" class="text-center" style="min-width: 50px;">Driver</th>
<th scope="col" class="text-center" style="min-width: 100px;">Points</th>
<th scope="col" class="text-center" style="min-width: 100px;">DNFs</th>
<th scope="col" class="text-center" style="min-width: 100px;">Place Delta</th>
</tr>
</thead>
<tbody>
{% for driver in points.drivers_sorted_by_points() %}
{% set driver_standing = points.wdc_standing_by_driver()[driver.name] %}
<tr class="{% if driver_standing == 1 %}table-danger{% endif %}">
<td class="text-center text-nowrap">{{ driver_standing }}</td>
<td class="text-center text-nowrap">{{ driver.name }}</td>
<td class="text-center text-nowrap">{{ points.total_driver_points_by(driver.name) }}</td>
<td class="text-center text-nowrap">{{ points.dnfs()[driver.name] }}</td>
<td class="text-center text-nowrap">{{ "%+d" % points.wdc_diff_2023_by(driver.name) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="card shadow-sm mb-2">
<div class="card-header">
Constructors
</div>
<div class="card-body">
<div class="d-inline-block overflow-x-scroll w-100">
<table class="table table-bordered table-sm table-responsive">
<thead>
<tr>
<th scope="col" class="text-center" style="min-width: 50px;">Place</th>
<th scope="col" class="text-center" style="min-width: 50px;">Team</th>
<th scope="col" class="text-center" style="min-width: 100px;">Points</th>
<th scope="col" class="text-center" style="min-width: 100px;">Place Delta</th>
</tr>
</thead>
<tbody>
{% for team in points.teams_sorted_by_points() %}
{% set team_standing = points.wcc_standing_by_team()[team.name] %}
<tr class="{% if team_standing == 1 %}table-danger{% endif %}">
<td class="text-center text-nowrap">{{ team_standing }}</td>
<td class="text-center text-nowrap">{{ team.name }}</td>
<td class="text-center text-nowrap">{{ points.total_team_points_by(team.name) }}</td>
<td class="text-center text-nowrap">{{ points.wcc_diff_2023_by(team.name) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="card shadow-sm mb-2">
<div class="card-header">
Driver history
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="driver-line-chart"></canvas>
</div>
<script>
function cumulative_driver_points(data) {
return new Chart(document.getElementById("driver-line-chart"), {
type: 'line',
data: data,
options: {
title: {
display: true,
text: 'History'
},
{#tension: 0,#}
responsive: true,
maintainAspectRatio: false,
pointRadius: 5,
pointHoverRadius: 10,
scales: {
y: {
min: 0,
{#max: 100#}
}
}
}
});
}
cumulative_driver_points({{ points.cumulative_driver_points_data() | safe }})
</script>
</div>
</div>
<div class="card shadow-sm mb-2">
<div class="card-header">
Team history
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="team-line-chart"></canvas>
</div>
<script>
function cumulative_team_points(data) {
return new Chart(document.getElementById("team-line-chart"), {
type: 'line',
data: data,
options: {
title: {
display: true,
text: 'History'
},
{#tension: 0,#}
responsive: true,
maintainAspectRatio: false,
pointRadius: 5,
pointHoverRadius: 10,
scales: {
y: {
min: 0,
{#max: 100#}
}
}
}
});
}
cumulative_team_points({{ points.cumulative_team_points_data() | safe }})
</script>
</div>
</div>
</div>
{% endblock body %}

View File

@ -6,10 +6,12 @@
{% block body %} {% block body %}
<div class="card shadow-sm"> <div class="card shadow-sm mb-2">
<div class="card-body"> <div class="card-header">
<h5 class="card-title">Add User</h5> Add user
</div>
<div class="card-body">
<form action="/user-add" method="post"> <form action="/user-add" method="post">
<div class="input-group"> <div class="input-group">
<div class="form-floating"> <div class="form-floating">
@ -25,10 +27,12 @@
</div> </div>
{% if model.all_users() | length > 0 %} {% if model.all_users() | length > 0 %}
<div class="card mt-2 shadow-sm"> <div class="card shadow-sm mb-2">
<div class="card-body"> <div class="card-header">
<h5 class="card-title">Registered Users</h5> Registered users
</div>
<div class="card-body">
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
{% for user in model.all_users() %} {% for user in model.all_users() %}
<li class="list-group-item">{{ user.name }}</li> <li class="list-group-item">{{ user.name }}</li>
@ -37,10 +41,12 @@
</div> </div>
</div> </div>
<div class="card mt-2 shadow-sm"> <div class="card shadow-sm mb-2">
<div class="card-body"> <div class="card-header">
<h5 class="card-title">Delete user</h5> Delete user
</div>
<div class="card-body">
<form action="/user-delete" method="post"> <form action="/user-delete" method="post">
<div class="input-group"> <div class="input-group">
<select class="form-control form-select" aria-label="select-delete-user" <select class="form-control form-select" aria-label="select-delete-user"
@ -66,16 +72,4 @@
</div> </div>
{% endif %} {% endif %}
{# <div class="card mt-2 border-danger shadow-sm">#}
{# <div class="card-body">#}
{# <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>#}
{# <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/static">Load static data</a>#}
{# <a class="btn btn-outline-danger" href="/load/dynamic">Load dynamic data</a>#}
{# </div>#}
{# </div>#}
{% endblock body %} {% endblock body %}

View File

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