Change file structure
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 50s
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 50s
This commit is contained in:
174
app/frontend/controller.py
Normal file
174
app/frontend/controller.py
Normal file
@ -0,0 +1,174 @@
|
||||
from typing import List
|
||||
from urllib.parse import unquote
|
||||
from flask import Flask, render_template, request, redirect
|
||||
from werkzeug import Response
|
||||
from app.database.model import Team, db
|
||||
from app.database.file_utils import reload_static_data, reload_dynamic_data, export_dynamic_data
|
||||
from app.frontend.template_model import TemplateModel
|
||||
from app.database.backend_model import delete_race_guess, update_race_guess, update_race_result, update_season_guess, update_user
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///formula10.db"
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.url_map.strict_slashes = False
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
|
||||
# TODO
|
||||
# General
|
||||
|
||||
# - 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?
|
||||
# - Make the season card grid left-aligned? So e.g. 2 cards are not spread over the whole screen with large gaps?
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def root() -> Response:
|
||||
return redirect("/race/Everyone")
|
||||
|
||||
|
||||
@app.route("/save/all")
|
||||
def save() -> Response:
|
||||
export_dynamic_data()
|
||||
return redirect("/")
|
||||
|
||||
|
||||
@app.route("/load/all")
|
||||
def load() -> Response:
|
||||
reload_static_data()
|
||||
reload_dynamic_data()
|
||||
return redirect("/")
|
||||
|
||||
|
||||
@app.route("/load/static")
|
||||
def load_static() -> Response:
|
||||
reload_static_data()
|
||||
return redirect("/")
|
||||
|
||||
|
||||
@app.route("/load/dynamic")
|
||||
def load_dynamic() -> Response:
|
||||
reload_dynamic_data()
|
||||
return redirect("/")
|
||||
|
||||
|
||||
@app.route("/race")
|
||||
def race_root() -> Response:
|
||||
return redirect("/race/Everyone")
|
||||
|
||||
|
||||
@app.route("/race/<user_name>")
|
||||
def race_active_user(user_name: str) -> str:
|
||||
user_name = unquote(user_name)
|
||||
model = TemplateModel()
|
||||
return render_template("race.jinja",
|
||||
active_user=model.user_by(user_name=user_name, ignore=["Everyone"]),
|
||||
model=model)
|
||||
|
||||
|
||||
@app.route("/race-guess/<race_name>/<user_name>", methods=["POST"])
|
||||
def race_guess_post(race_name: str, user_name: str) -> Response:
|
||||
race_name = unquote(race_name)
|
||||
user_name = unquote(user_name)
|
||||
|
||||
pxx: str | None = request.form.get("pxxselect")
|
||||
dnf: str | None = request.form.get("dnfselect")
|
||||
|
||||
return update_race_guess(race_name, user_name, pxx, dnf)
|
||||
|
||||
|
||||
@app.route("/race-guess-delete/<race_name>/<user_name>", methods=["POST"])
|
||||
def race_guess_delete_post(race_name: str, user_name: str) -> Response:
|
||||
race_name = unquote(race_name)
|
||||
user_name = unquote(user_name)
|
||||
|
||||
return delete_race_guess(race_name, user_name)
|
||||
|
||||
|
||||
@app.route("/season")
|
||||
def season_root() -> Response:
|
||||
return redirect("/season/Everyone")
|
||||
|
||||
|
||||
@app.route("/season/<user_name>")
|
||||
def season_active_user(user_name: str) -> str:
|
||||
user_name = unquote(user_name)
|
||||
model = TemplateModel()
|
||||
return render_template("season.jinja",
|
||||
active_user=model.user_by(user_name=user_name, ignore=["Everyone"]),
|
||||
model=model)
|
||||
|
||||
|
||||
@app.route("/season-guess/<user_name>", methods=["POST"])
|
||||
def season_guess_post(user_name: str) -> Response:
|
||||
user_name = unquote(user_name)
|
||||
guesses: List[str | None] = [
|
||||
request.form.get("hottakeselect"),
|
||||
request.form.get("p2select"),
|
||||
request.form.get("overtakeselect"),
|
||||
request.form.get("dnfselect"),
|
||||
request.form.get("gainedselect"),
|
||||
request.form.get("lostselect")
|
||||
]
|
||||
team_winner_guesses: List[str | None] = [
|
||||
request.form.get(f"teamwinner-{team.name}") for team in db.session.query(Team).all()
|
||||
]
|
||||
podium_driver_guesses: List[str] = request.form.getlist("podiumdrivers")
|
||||
|
||||
return update_season_guess(user_name, guesses, team_winner_guesses, podium_driver_guesses)
|
||||
|
||||
|
||||
@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()
|
||||
return render_template("enter.jinja",
|
||||
active_result=model.race_result_by(race_name=race_name),
|
||||
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")
|
||||
def user_root() -> str:
|
||||
model = TemplateModel()
|
||||
return render_template("users.jinja",
|
||||
model=model)
|
||||
|
||||
|
||||
@app.route("/user-add", methods=["POST"])
|
||||
def user_add_post() -> Response:
|
||||
username: str | None = request.form.get("select-add-user")
|
||||
return update_user(username, add=True)
|
||||
|
||||
|
||||
@app.route("/user-delete", methods=["POST"])
|
||||
def user_delete_post() -> Response:
|
||||
username: str | None = request.form.get("select-delete-user")
|
||||
return update_user(username, delete=True)
|
252
app/frontend/template_model.py
Normal file
252
app/frontend/template_model.py
Normal file
@ -0,0 +1,252 @@
|
||||
from typing import List, Callable, Dict, overload
|
||||
from sqlalchemy import desc
|
||||
from app.database.model import User, RaceResult, RaceGuess, Race, Driver, Team, SeasonGuess, db
|
||||
from app.database.validation_utils import find_first_or_none, find_multiple, find_single, find_single_or_none
|
||||
|
||||
|
||||
# This could also be moved to database_utils (at least partially), but I though the template should cache the database responses
|
||||
class TemplateModel:
|
||||
"""
|
||||
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
|
||||
|
||||
def all_users(self) -> List[User]:
|
||||
"""
|
||||
Returns a list of all users in the database.
|
||||
"""
|
||||
if self._all_users is None:
|
||||
self._all_users = db.session.query(User).all()
|
||||
|
||||
return self._all_users
|
||||
|
||||
@overload
|
||||
def user_by(self, *, user_name: str) -> User:
|
||||
"""
|
||||
Tries to obtain the user object for a specific username.
|
||||
"""
|
||||
return self.user_by(user_name=user_name)
|
||||
|
||||
@overload
|
||||
def user_by(self, *, user_name: str, ignore: List[str]) -> User | None:
|
||||
"""
|
||||
Tries to obtain the user object for a specific username, but ignores certain usernames.
|
||||
"""
|
||||
return self.user_by(user_name=user_name, ignore=ignore)
|
||||
|
||||
def user_by(self, *, user_name: str, ignore: List[str] | None = None) -> User | None:
|
||||
if ignore is None:
|
||||
ignore = []
|
||||
|
||||
if len(ignore) > 0 and user_name in ignore:
|
||||
return None
|
||||
|
||||
predicate: Callable[[User], bool] = lambda user: user.name == user_name
|
||||
return find_single(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 = db.session.query(RaceResult).join(RaceResult.race).order_by(desc(Race.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.
|
||||
"""
|
||||
predicate: Callable[[RaceResult], bool] = lambda result: result.race.name == race_name
|
||||
return find_single_or_none(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 = db.session.query(RaceGuess).all()
|
||||
|
||||
return self._all_race_guesses
|
||||
|
||||
@overload
|
||||
def race_guesses_by(self, *, user_name: str) -> List[RaceGuess]:
|
||||
"""
|
||||
Returns a list of all race guesses made by a specific user.
|
||||
"""
|
||||
return self.race_guesses_by(user_name=user_name)
|
||||
|
||||
@overload
|
||||
def race_guesses_by(self, *, race_name: str) -> List[RaceGuess]:
|
||||
"""
|
||||
Returns a list of all race guesses made for a specific race.
|
||||
"""
|
||||
return self.race_guesses_by(race_name=race_name)
|
||||
|
||||
@overload
|
||||
def race_guesses_by(self, *, user_name: str, race_name: str) -> RaceGuess | None:
|
||||
"""
|
||||
Returns a single race guess by a specific user for a specific race, or None, if this guess doesn't exist.
|
||||
"""
|
||||
return self.race_guesses_by(user_name=user_name, race_name=race_name)
|
||||
|
||||
@overload
|
||||
def race_guesses_by(self) -> Dict[str, Dict[str, RaceGuess]]:
|
||||
"""
|
||||
Returns a dictionary that maps race-ids to user-id - guess dictionaries.
|
||||
"""
|
||||
return self.race_guesses_by()
|
||||
|
||||
def race_guesses_by(self, *, user_name: str | None = None, race_name: str | None = None) -> RaceGuess | List[RaceGuess] | Dict[str, Dict[str, RaceGuess]] | None:
|
||||
# List of all guesses by a single user
|
||||
if user_name is not None and race_name is None:
|
||||
predicate: Callable[[RaceGuess], bool] = lambda guess: guess.user_name == user_name
|
||||
return find_multiple(predicate, self.all_race_guesses())
|
||||
|
||||
# List of all guesses for a single race
|
||||
if user_name is None and race_name is not None:
|
||||
predicate: Callable[[RaceGuess], bool] = lambda guess: guess.race_name == race_name
|
||||
return find_multiple(predicate, self.all_race_guesses())
|
||||
|
||||
# Guess for a single race by a single user
|
||||
if user_name is not None and race_name is not None:
|
||||
predicate: Callable[[RaceGuess], bool] = lambda guess: guess.user_name == user_name and guess.race_name == race_name
|
||||
return find_single_or_none(predicate, self.all_race_guesses())
|
||||
|
||||
# Dict with all guesses
|
||||
if user_name is None and race_name is None:
|
||||
guesses_by: Dict[str, Dict[str, RaceGuess]] = dict()
|
||||
guess: RaceGuess
|
||||
|
||||
for guess in self.all_race_guesses():
|
||||
if guess.race_name not in guesses_by:
|
||||
guesses_by[guess.race_name] = dict()
|
||||
|
||||
guesses_by[guess.race_name][guess.user_name] = guess
|
||||
|
||||
return guesses_by
|
||||
|
||||
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 = db.session.query(SeasonGuess).all()
|
||||
|
||||
return self._all_season_guesses
|
||||
|
||||
@overload
|
||||
def season_guesses_by(self, *, user_name: str) -> SeasonGuess:
|
||||
"""
|
||||
Returns the season guess made by a specific user.
|
||||
"""
|
||||
return self.season_guesses_by(user_name=user_name)
|
||||
|
||||
@overload
|
||||
def season_guesses_by(self) -> Dict[str, SeasonGuess]:
|
||||
"""
|
||||
Returns a dictionary of season guesses mapped to usernames.
|
||||
"""
|
||||
return self.season_guesses_by()
|
||||
|
||||
def season_guesses_by(self, *, user_name: str | None = None) -> SeasonGuess | Dict[str, SeasonGuess] | None:
|
||||
if user_name is not None:
|
||||
predicate: Callable[[SeasonGuess], bool] = lambda guess: guess.user_name == user_name
|
||||
return find_single_or_none(predicate, self.all_season_guesses())
|
||||
|
||||
if user_name is None:
|
||||
guesses_by: Dict[str, SeasonGuess] = dict()
|
||||
guess: SeasonGuess
|
||||
|
||||
for guess in self.all_season_guesses():
|
||||
guesses_by[guess.user_name] = guess
|
||||
|
||||
return guesses_by
|
||||
|
||||
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 = db.session.query(Race).order_by(desc(Race.number)).all()
|
||||
|
||||
return self._all_races
|
||||
|
||||
def first_race_without_result(self) -> Race | None:
|
||||
"""
|
||||
Returns the first race-object with no associated race result.
|
||||
"""
|
||||
results: List[RaceResult] = self.all_race_results()
|
||||
if len(results) == 0:
|
||||
return self.all_races()[-1] # all_races is sorted descending by number
|
||||
|
||||
most_recent_result: RaceResult = results[0]
|
||||
predicate: Callable[[Race], bool] = lambda race: race.number == most_recent_result.race.number + 1
|
||||
|
||||
return find_first_or_none(predicate, self.all_races())
|
||||
|
||||
def all_teams(self) -> List[Team]:
|
||||
"""
|
||||
Returns a list of all teams in the database.
|
||||
"""
|
||||
if self._all_teams is None:
|
||||
self._all_teams = db.session.query(Team).all()
|
||||
|
||||
return self._all_teams
|
||||
|
||||
def all_drivers(self) -> List[Driver]:
|
||||
"""
|
||||
Returns a list of all drivers in the database, including the NONE driver.
|
||||
"""
|
||||
if self._all_drivers is None:
|
||||
self._all_drivers = db.session.query(Driver).all()
|
||||
|
||||
return self._all_drivers
|
||||
|
||||
def all_drivers_except_none(self) -> List[Driver]:
|
||||
"""
|
||||
Returns a list of all drivers in the database, excluding the NONE driver.
|
||||
"""
|
||||
predicate: Callable[[Driver], bool] = lambda driver: driver.name != "None"
|
||||
return find_multiple(predicate, self.all_drivers())
|
||||
|
||||
@overload
|
||||
def drivers_by(self, *, team_name: str) -> List[Driver]:
|
||||
"""
|
||||
Returns a list of all drivers driving for a certain team.
|
||||
"""
|
||||
return self.drivers_by(team_name=team_name)
|
||||
|
||||
@overload
|
||||
def drivers_by(self) -> Dict[str, List[Driver]]:
|
||||
"""
|
||||
Returns a dictionary of drivers mapped to team names.
|
||||
"""
|
||||
return self.drivers_by()
|
||||
|
||||
def drivers_by(self, *, team_name: str | None = None) -> List[Driver] | Dict[str, List[Driver]]:
|
||||
if team_name is not None:
|
||||
predicate: Callable[[Driver], bool] = lambda driver: driver.team.name == team_name
|
||||
return find_multiple(predicate, self.all_drivers_except_none(), 2)
|
||||
|
||||
if team_name is None:
|
||||
drivers_by: Dict[str, List[Driver]] = dict()
|
||||
driver: Driver
|
||||
team: Team
|
||||
|
||||
for team in self.all_teams():
|
||||
drivers_by[team.name] = []
|
||||
for driver in self.all_drivers_except_none():
|
||||
drivers_by[driver.team.name] += [driver]
|
||||
|
||||
return drivers_by
|
||||
|
||||
raise Exception("drivers_by encountered illegal combination of arguments")
|
Reference in New Issue
Block a user