diff --git a/formula10/__init__.py b/formula10/__init__.py index 9bb676e..03469b3 100644 --- a/formula10/__init__.py +++ b/formula10/__init__.py @@ -17,6 +17,7 @@ db.init_app(app) # 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.season_controller +import formula10.controller.statistics_controller import formula10.controller.rules_controller import formula10.controller.admin_controller import formula10.controller.error_controller @@ -25,11 +26,14 @@ import formula10.controller.error_controller # TODO # General +# Create a model baseclass that contains the cached teams/drivers/races etc., so the points + template model can be derived from it + # Statistics # - Auto calculate points -# - Display points somewhere in race table? -# - Highlight currently correct values for some season guesses (e.g. current most dnfs) +# - Display points somewhere in race table? Below the name in the table header. +# - Highlight currently correct values for some season guesses (e.g. current most dnfs, team winners, podiums) # - Generate static diagram using chart.js + templating the js (funny yikes) +# - Which driver was voted most for dnf? # Possible but probably not # - Show cards of previous race results, like with season guesses? diff --git a/formula10/logic/points_model.py b/formula10/controller/__init__.py similarity index 100% rename from formula10/logic/points_model.py rename to formula10/controller/__init__.py diff --git a/formula10/controller/admin_controller.py b/formula10/controller/admin_controller.py index fa8734d..e8c557c 100644 --- a/formula10/controller/admin_controller.py +++ b/formula10/controller/admin_controller.py @@ -5,7 +5,7 @@ from werkzeug import Response from formula10.database.update_queries import update_race_result, update_user from formula10.database.import_export import export_dynamic_data, reload_static_data -from formula10.frontend.template_model import TemplateModel +from formula10.domain.template_model import TemplateModel from formula10 import app diff --git a/formula10/controller/error_controller.py b/formula10/controller/error_controller.py index 52f12b5..69da087 100644 --- a/formula10/controller/error_controller.py +++ b/formula10/controller/error_controller.py @@ -2,7 +2,7 @@ from typing import cast from flask import redirect, render_template, session from werkzeug import Response -from formula10.frontend.template_model import TemplateModel +from formula10.domain.template_model import TemplateModel from formula10 import app def error_redirect(error_message: str) -> Response: diff --git a/formula10/controller/race_controller.py b/formula10/controller/race_controller.py index 99b890c..3695b74 100644 --- a/formula10/controller/race_controller.py +++ b/formula10/controller/race_controller.py @@ -3,7 +3,7 @@ from flask import redirect, render_template, request from werkzeug import Response from formula10.database.update_queries import delete_race_guess, update_race_guess -from formula10.frontend.template_model import TemplateModel +from formula10.domain.template_model import TemplateModel from formula10 import app diff --git a/formula10/controller/rules_controller.py b/formula10/controller/rules_controller.py index c57965d..372dd4b 100644 --- a/formula10/controller/rules_controller.py +++ b/formula10/controller/rules_controller.py @@ -1,7 +1,7 @@ from flask import render_template from formula10 import app -from formula10.frontend.template_model import TemplateModel +from formula10.domain.template_model import TemplateModel @app.route("/rules") def rules_root() -> str: diff --git a/formula10/controller/season_controller.py b/formula10/controller/season_controller.py index aec4892..8830c66 100644 --- a/formula10/controller/season_controller.py +++ b/formula10/controller/season_controller.py @@ -5,8 +5,8 @@ from werkzeug import Response from formula10.database.model.db_team import DbTeam from formula10.database.update_queries import update_season_guess -from formula10.frontend.model.team import NONE_TEAM -from formula10.frontend.template_model import TemplateModel +from formula10.domain.model.team import NONE_TEAM +from formula10.domain.template_model import TemplateModel from formula10 import app, db diff --git a/formula10/controller/statistics_controller.py b/formula10/controller/statistics_controller.py new file mode 100644 index 0000000..915b925 --- /dev/null +++ b/formula10/controller/statistics_controller.py @@ -0,0 +1,9 @@ +from flask import render_template +from formula10 import app +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) + + return render_template("statistics.jinja", model=model) \ No newline at end of file diff --git a/formula10/database/__init__.py b/formula10/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/formula10/database/model/__init__.py b/formula10/database/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/formula10/database/validation.py b/formula10/database/validation.py index 3a0c50a..dffaafa 100644 --- a/formula10/database/validation.py +++ b/formula10/database/validation.py @@ -3,7 +3,7 @@ from typing import Any, Callable, Iterable, List, TypeVar, overload from formula10.database.model.db_race import DbRace from formula10 import db -from formula10.frontend.model.race import Race +from formula10.domain.model.race import Race _T = TypeVar("_T") diff --git a/formula10/domain/__init__.py b/formula10/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/formula10/domain/domain_model.py b/formula10/domain/domain_model.py new file mode 100644 index 0000000..c4a0d01 --- /dev/null +++ b/formula10/domain/domain_model.py @@ -0,0 +1,118 @@ +from typing import Callable, List +from sqlalchemy import desc + +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 +from formula10.database.validation import find_multiple_strict +from formula10.domain.model.driver import NONE_DRIVER, Driver +from formula10.domain.model.race import Race +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.team import NONE_TEAM, Team +from formula10.domain.model.user import User +from formula10 import db + + +class Model(): + _all_users: List[User] | None = None + _all_race_results: List[RaceResult] | None = None + _all_race_guesses: List[RaceGuess] | None = None + _all_season_guesses: List[SeasonGuess] | None = None + _all_races: List[Race] | None = None + _all_drivers: List[Driver] | None = None + _all_teams: List[Team] | None = None + + def all_users(self) -> List[User]: + """ + Returns a list of all users in the database. + """ + if self._all_users is None: + self._all_users = [ + User.from_db_user(db_user) + for db_user in db.session.query(DbUser).filter_by(enabled=True).all() + ] + + return self._all_users + + def all_race_results(self) -> List[RaceResult]: + """ + 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 all_race_guesses(self) -> List[RaceGuess]: + """ + 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 + + def all_season_guesses(self) -> List[SeasonGuess]: + if self._all_season_guesses is None: + self._all_season_guesses = [ + SeasonGuess.from_db_season_guess(db_season_guess) + for db_season_guess in db.session.query(DbSeasonGuess).join(DbSeasonGuess.user).filter_by(enabled=True).all() # Ignore disabled users + ] + + return self._all_season_guesses + + def all_races(self) -> List[Race]: + """ + Returns a list of all races in 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 all_drivers(self, *, include_none: bool) -> List[Driver]: + """ + 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_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) \ No newline at end of file diff --git a/formula10/domain/model/__init__.py b/formula10/domain/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/formula10/frontend/model/driver.py b/formula10/domain/model/driver.py similarity index 94% rename from formula10/frontend/model/driver.py rename to formula10/domain/model/driver.py index b1df1dd..0b951f4 100644 --- a/formula10/frontend/model/driver.py +++ b/formula10/domain/model/driver.py @@ -1,7 +1,7 @@ from urllib.parse import quote 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(): diff --git a/formula10/frontend/model/race.py b/formula10/domain/model/race.py similarity index 100% rename from formula10/frontend/model/race.py rename to formula10/domain/model/race.py diff --git a/formula10/frontend/model/race_guess.py b/formula10/domain/model/race_guess.py similarity index 88% rename from formula10/frontend/model/race_guess.py rename to formula10/domain/model/race_guess.py index 4d1cd29..4e67cd7 100644 --- a/formula10/frontend/model/race_guess.py +++ b/formula10/domain/model/race_guess.py @@ -1,7 +1,7 @@ from formula10.database.model.db_race_guess import DbRaceGuess -from formula10.frontend.model.driver import Driver -from formula10.frontend.model.race import Race -from formula10.frontend.model.user import User +from formula10.domain.model.driver import Driver +from formula10.domain.model.race import Race +from formula10.domain.model.user import User class RaceGuess(): diff --git a/formula10/frontend/model/race_result.py b/formula10/domain/model/race_result.py similarity index 97% rename from formula10/frontend/model/race_result.py rename to formula10/domain/model/race_result.py index 41bc9d2..28dd503 100644 --- a/formula10/frontend/model/race_result.py +++ b/formula10/domain/model/race_result.py @@ -3,8 +3,8 @@ from typing import Dict, List from formula10.database.common_queries import find_single_driver_strict from formula10.database.model.db_race_result import DbRaceResult -from formula10.frontend.model.driver import NONE_DRIVER, Driver -from formula10.frontend.model.race import Race +from formula10.domain.model.driver import NONE_DRIVER, Driver +from formula10.domain.model.race import Race class RaceResult: diff --git a/formula10/frontend/model/season_guess.py b/formula10/domain/model/season_guess.py similarity index 96% rename from formula10/frontend/model/season_guess.py rename to formula10/domain/model/season_guess.py index 6ecd4d9..98e2956 100644 --- a/formula10/frontend/model/season_guess.py +++ b/formula10/domain/model/season_guess.py @@ -2,9 +2,9 @@ import json from typing import List from formula10.database.common_queries import find_single_driver_strict from formula10.database.model.db_season_guess import DbSeasonGuess -from formula10.frontend.model.driver import Driver -from formula10.frontend.model.team import Team -from formula10.frontend.model.user import User +from formula10.domain.model.driver import Driver +from formula10.domain.model.team import Team +from formula10.domain.model.user import User class SeasonGuess(): diff --git a/formula10/frontend/model/team.py b/formula10/domain/model/team.py similarity index 100% rename from formula10/frontend/model/team.py rename to formula10/domain/model/team.py diff --git a/formula10/frontend/model/user.py b/formula10/domain/model/user.py similarity index 100% rename from formula10/frontend/model/user.py rename to formula10/domain/model/user.py diff --git a/formula10/domain/points_model.py b/formula10/domain/points_model.py new file mode 100644 index 0000000..684057c --- /dev/null +++ b/formula10/domain/points_model.py @@ -0,0 +1,5 @@ +from formula10.domain.domain_model import Model + +class PointsModel(Model): + def __init__(self): + Model.__init__(self) \ No newline at end of file diff --git a/formula10/frontend/template_model.py b/formula10/domain/template_model.py similarity index 68% rename from formula10/frontend/template_model.py rename to formula10/domain/template_model.py index e64a8f6..3ee3d2f 100644 --- a/formula10/frontend/template_model.py +++ b/formula10/domain/template_model.py @@ -1,37 +1,21 @@ from typing import List, Callable, Dict, overload -from sqlalchemy import desc -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 -from formula10.frontend.model.driver import NONE_DRIVER, Driver -from formula10.frontend.model.race import Race -from formula10.frontend.model.race_guess import RaceGuess -from formula10.frontend.model.race_result import RaceResult -from formula10.frontend.model.season_guess import SeasonGuess -from formula10.frontend.model.team import NONE_TEAM, Team -from formula10.frontend.model.user import User +from formula10.domain.domain_model import Model +from formula10.domain.model.driver import NONE_DRIVER, Driver +from formula10.domain.model.race import Race +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.team import NONE_TEAM, Team +from formula10.domain.model.user import User from formula10.database.validation import find_first_else_none, find_multiple_strict, find_single_strict, find_single_or_none_strict, race_has_started -from formula10 import db -class TemplateModel: +class TemplateModel(Model): """ This class bundles all data required from inside a template. """ - _all_users: List[User] | None = None - _all_race_results: List[RaceResult] | None = None - _all_race_guesses: List[RaceGuess] | None = None - _all_season_guesses: List[SeasonGuess] | None = None - _all_races: List[Race] | None = None - _all_drivers: List[Driver] | None = None - _all_teams: List[Team] | None = None - active_user: User | None = None active_result: RaceResult | None = None @@ -57,18 +41,6 @@ class TemplateModel: 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]: - """ - Returns a list of all users in the database. - """ - if self._all_users is None: - self._all_users = [ - User.from_db_user(db_user) - for db_user in db.session.query(DbUser).filter_by(enabled=True).all() - ] - - return self._all_users - def all_users_or_active_user(self) -> List[User]: if self.active_user is not None: return [self.active_user] @@ -99,18 +71,6 @@ class TemplateModel: predicate: Callable[[User], bool] = lambda user: user.name == user_name return find_single_strict(predicate, self.all_users()) - def all_race_results(self) -> List[RaceResult]: - """ - 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: """ Tries to obtain the race result corresponding to a race name. @@ -118,18 +78,6 @@ class TemplateModel: predicate: Callable[[RaceResult], bool] = lambda result: result.race.name == race_name return find_single_or_none_strict(predicate, self.all_race_results()) - def all_race_guesses(self) -> List[RaceGuess]: - """ - 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 def race_guesses_by(self, *, user_name: str) -> List[RaceGuess]: """ @@ -189,15 +137,6 @@ class TemplateModel: raise Exception("race_guesses_by encountered illegal combination of arguments") - def all_season_guesses(self) -> List[SeasonGuess]: - 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 - @overload def season_guesses_by(self, *, user_name: str) -> SeasonGuess: """ @@ -228,18 +167,6 @@ class TemplateModel: raise Exception("season_guesses_by encountered illegal combination of arguments") - def all_races(self) -> List[Race]: - """ - Returns a list of all races in the database. - """ - if self._all_races is None: - self._all_races = [ - Race.from_db_race(db_race) - for db_race in db.session.query(DbRace).order_by(desc(DbRace.number)).all() - ] - - return self._all_races - def first_race_without_result(self) -> Race | None: """ Returns the first race-object with no associated race result. @@ -273,41 +200,9 @@ class TemplateModel: else: return self.all_races()[0].name_sanitized - def all_teams(self, *, include_none: bool) -> List[Team]: - """ - Returns a list of all teams in the database. - """ - if self._all_teams is None: - self._all_teams = [ - Team.from_db_team(db_team) - for db_team in db.session.query(DbTeam).all() - ] - - if include_none: - return self._all_teams - else: - predicate: Callable[[Team], bool] = lambda team: team != NONE_TEAM - return find_multiple_strict(predicate, self._all_teams) - def none_team(self) -> Team: return NONE_TEAM - def all_drivers(self, *, include_none: bool) -> List[Driver]: - """ - 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) diff --git a/formula10/templates/statistics.jinja b/formula10/templates/statistics.jinja new file mode 100644 index 0000000..22e2cab --- /dev/null +++ b/formula10/templates/statistics.jinja @@ -0,0 +1,33 @@ +{% extends 'base.jinja' %} + +{% block title %}Formula 10 - Leaderboard{% endblock title %} + +{% set active_page = "/graphs" %} + +{% block body %} + +
+
+
Leaderboard
+ + {# Table that lists each users + Total Points (?), Race guesses points, Season guesses points (missing overtakes + hottake), number of guesses that yielded points, average points per guess #} +
+
+ +
+
+
History
+ + {# Line chart of point history with a line per user #} +
+
+ +
+
+
Statistics
+ + {# Various statistics: Driver voted most for DNF #} +
+
+ +{% endblock body %}