Compare commits
4 Commits
1c126796a9
...
d0c7ba242c
| Author | SHA1 | Date | |
|---|---|---|---|
| d0c7ba242c | |||
| 595b49c2b5 | |||
| a99b9ff005 | |||
| e9c916305e |
165
formula10.py
165
formula10.py
@ -15,12 +15,13 @@ db.init_app(app)
|
|||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
# General
|
# General
|
||||||
# - Move guessed place to leftmost column and display actual finishing position of driver instead
|
# - Store standing/dnf orders as dicts, since lists lose their order
|
||||||
# - Show coming race in table, to give better feedback once a user has locked in a guess
|
# - Make user headers in race table clickable, to reach the specific page. Do the same for the season cards
|
||||||
# - Only allow guess entering in user-specific page
|
# - When showing correct guesses in green, show semi-correct ones in a weaker tone (probably need to prepare those here, instead of in the template)
|
||||||
# - Remove whitespace from usernames
|
# - Also show previous race results, and allow to change them. Or at least, allow editing the current one and show current state (do it like the activeuser select for results)
|
||||||
|
# - Remove whitespace from usernames + races. Sanitization should only happen inside the templates + endpoint controllers, for the URLs
|
||||||
|
# - Add doc comments to model
|
||||||
|
|
||||||
# - Sortable list to enter full race results (need 7 positions to calculate points) => remove from race page
|
|
||||||
# - Make the season card grid left-aligned? So e.g. 2 cards are not spread over the whole screen with large gaps?
|
# - Make the season card grid left-aligned? So e.g. 2 cards are not spread over the whole screen with large gaps?
|
||||||
# - Choose "place to guess" late before the race?
|
# - Choose "place to guess" late before the race?
|
||||||
# - Timer until season picks lock + next race timer
|
# - Timer until season picks lock + next race timer
|
||||||
@ -28,7 +29,7 @@ db.init_app(app)
|
|||||||
|
|
||||||
# Statistics page
|
# Statistics page
|
||||||
# - Auto calculate points
|
# - Auto calculate points
|
||||||
# - Generate static diagram using chart.js + templating the js (yikes)
|
# - Generate static diagram using chart.js + templating the js (funny yikes)
|
||||||
|
|
||||||
# Rules page
|
# Rules page
|
||||||
|
|
||||||
@ -75,40 +76,39 @@ def guessuserraceresults(username):
|
|||||||
raceresults: List[RaceResult] = RaceResult.query.all()[::-1]
|
raceresults: List[RaceResult] = RaceResult.query.all()[::-1]
|
||||||
drivers: List[Driver] = Driver.query.all()
|
drivers: List[Driver] = Driver.query.all()
|
||||||
|
|
||||||
# Select User
|
print(raceresults)
|
||||||
# chosenusers = users
|
|
||||||
# if username != "Everyone":
|
|
||||||
# chosenusers = [user for user in users if user.name == username]
|
|
||||||
|
|
||||||
guesses = dict()
|
guesses: Dict[int, Dict[str, RaceGuess]] = dict()
|
||||||
|
guess: RaceGuess
|
||||||
for guess in RaceGuess.query.all():
|
for guess in RaceGuess.query.all():
|
||||||
if guess.race_id not in guesses:
|
if guess.race_id not in guesses:
|
||||||
guesses[guess.race_id] = dict()
|
guesses[guess.race_id] = dict()
|
||||||
|
|
||||||
guesses[guess.race_id][guess.user_id] = guess
|
guesses[guess.race_id][guess.user_id] = guess
|
||||||
|
|
||||||
# nextid = raceresults[0].race_id + 1 if len(raceresults) > 0 else 1
|
nextid = raceresults[0].race_id + 1 if len(raceresults) > 0 else 1
|
||||||
# nextrace: Race = Race.query.filter_by(id=nextid).first()
|
nextrace: Race | None = Race.query.filter_by(id=nextid).first()
|
||||||
|
|
||||||
return render_template("race.jinja",
|
return render_template("race.jinja",
|
||||||
users=users,
|
users=users,
|
||||||
drivers=drivers,
|
drivers=drivers,
|
||||||
raceresults=raceresults,
|
raceresults=raceresults,
|
||||||
guesses=guesses,
|
guesses=guesses,
|
||||||
activeuser=activeuser)
|
activeuser=activeuser,
|
||||||
|
currentrace=nextrace)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/guessrace/<raceid>/<username>", methods=["POST"])
|
@app.route("/guessrace/<raceid>/<username>", methods=["POST"])
|
||||||
def guessrace(raceid, username):
|
def guessrace(raceid, username):
|
||||||
pxx = request.form.get("pxxselect")
|
pxx: str | None = request.form.get("pxxselect")
|
||||||
dnf = request.form.get("dnfselect")
|
dnf: str | None = request.form.get("dnfselect")
|
||||||
|
|
||||||
if pxx is None or dnf is None:
|
if pxx is None or dnf is None:
|
||||||
return redirect("/race")
|
return redirect(f"/race/{username}")
|
||||||
|
|
||||||
if RaceResult.query.filter_by(race_id=raceid).first() is not None:
|
if RaceResult.query.filter_by(race_id=raceid).first() is not None:
|
||||||
print("Error: Can't guess race result if the race result is already known!")
|
print("Error: Can't guess race result if the race result is already known!")
|
||||||
return redirect("/race")
|
return redirect(f"/race/{username}")
|
||||||
|
|
||||||
raceguess: RaceGuess | None = RaceGuess.query.filter_by(user_id=username, race_id=raceid).first()
|
raceguess: RaceGuess | None = RaceGuess.query.filter_by(user_id=username, race_id=raceid).first()
|
||||||
|
|
||||||
@ -125,30 +125,6 @@ def guessrace(raceid, username):
|
|||||||
return redirect("/race")
|
return redirect("/race")
|
||||||
|
|
||||||
|
|
||||||
# @app.route("/enterresult/<raceid>", methods=["POST"])
|
|
||||||
# def enterresult(raceid):
|
|
||||||
# pxx = request.form.get("pxxselect")
|
|
||||||
# dnf = request.form.get("dnfselect")
|
|
||||||
#
|
|
||||||
# if pxx is None or dnf is None:
|
|
||||||
# return redirect("/race")
|
|
||||||
#
|
|
||||||
# raceresult: RaceResult | None = RaceResult.query.filter_by(race_id=raceid).first()
|
|
||||||
#
|
|
||||||
# if raceresult is not None:
|
|
||||||
# print("RaceResult already exists!")
|
|
||||||
# return redirect("/race")
|
|
||||||
#
|
|
||||||
# raceresult = RaceResult()
|
|
||||||
# raceresult.race_id = raceid
|
|
||||||
# raceresult.pxx_id = pxx
|
|
||||||
# raceresult.dnf_id = dnf
|
|
||||||
# db.session.add(raceresult)
|
|
||||||
# db.session.commit()
|
|
||||||
#
|
|
||||||
# return redirect("/race")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/season")
|
@app.route("/season")
|
||||||
def guessseasonresults():
|
def guessseasonresults():
|
||||||
return redirect("/season/Everyone")
|
return redirect("/season/Everyone")
|
||||||
@ -164,12 +140,12 @@ def guessuserseasonresults(username):
|
|||||||
# Remove NONE driver
|
# Remove NONE driver
|
||||||
drivers = [driver for driver in drivers if driver.name != "NONE"]
|
drivers = [driver for driver in drivers if driver.name != "NONE"]
|
||||||
|
|
||||||
guesses = dict()
|
guesses: Dict[str, SeasonGuess] = dict()
|
||||||
guess: SeasonGuess
|
guess: SeasonGuess
|
||||||
for guess in SeasonGuess.query.all():
|
for guess in SeasonGuess.query.all():
|
||||||
guesses[guess.user_id] = guess
|
guesses[guess.user_id] = guess
|
||||||
|
|
||||||
driverpairs = dict()
|
driverpairs: Dict[str, List[Driver]] = dict()
|
||||||
team: Team
|
team: Team
|
||||||
for team in teams:
|
for team in teams:
|
||||||
driverpairs[team.name] = []
|
driverpairs[team.name] = []
|
||||||
@ -187,8 +163,8 @@ def guessuserseasonresults(username):
|
|||||||
|
|
||||||
|
|
||||||
@app.route("/guessseason/<username>", methods=["POST"])
|
@app.route("/guessseason/<username>", methods=["POST"])
|
||||||
def guessseason(username):
|
def guessseason(username: str):
|
||||||
guesses = [
|
guesses: List[str | None] = [
|
||||||
request.form.get("hottakeselect"),
|
request.form.get("hottakeselect"),
|
||||||
request.form.get("p2select"),
|
request.form.get("p2select"),
|
||||||
request.form.get("overtakeselect"),
|
request.form.get("overtakeselect"),
|
||||||
@ -196,10 +172,10 @@ def guessseason(username):
|
|||||||
request.form.get("gainedselect"),
|
request.form.get("gainedselect"),
|
||||||
request.form.get("lostselect")
|
request.form.get("lostselect")
|
||||||
]
|
]
|
||||||
teamwinnerguesses = [
|
teamwinnerguesses: List[str | None] = [
|
||||||
request.form.get(f"teamwinner-{team.name}") for team in Team.query.all()
|
request.form.get(f"teamwinner-{team.name}") for team in Team.query.all()
|
||||||
]
|
]
|
||||||
podiumdriverguesses = request.form.getlist("podiumdrivers")
|
podiumdriverguesses: List[str] = request.form.getlist("podiumdrivers")
|
||||||
|
|
||||||
if any(guess is None for guess in guesses + teamwinnerguesses):
|
if any(guess is None for guess in guesses + teamwinnerguesses):
|
||||||
print("Error: /guessseason could not obtain request data!")
|
print("Error: /guessseason could not obtain request data!")
|
||||||
@ -213,7 +189,7 @@ def guessseason(username):
|
|||||||
teamwinners = TeamWinners()
|
teamwinners = TeamWinners()
|
||||||
db.session.add(teamwinners)
|
db.session.add(teamwinners)
|
||||||
|
|
||||||
teamwinners.winner_ids = teamwinnerguesses
|
teamwinners.winner_ids = teamwinnerguesses # Pylance throws error, but nullcheck is done
|
||||||
teamwinners.user_id = username
|
teamwinners.user_id = username
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@ -234,27 +210,86 @@ def guessseason(username):
|
|||||||
seasonguess.user_id = username
|
seasonguess.user_id = username
|
||||||
db.session.add(seasonguess)
|
db.session.add(seasonguess)
|
||||||
|
|
||||||
seasonguess.hot_take = guesses[0]
|
seasonguess.hot_take = guesses[0] # Pylance throws error but nullcheck is done
|
||||||
seasonguess.p2_constructor_id = guesses[1]
|
seasonguess.p2_constructor_id = guesses[1] # Pylance throws error but nullcheck is done
|
||||||
seasonguess.most_overtakes_id = guesses[2]
|
seasonguess.most_overtakes_id = guesses[2] # Pylance throws error but nullcheck is done
|
||||||
seasonguess.most_dnfs_id = guesses[3]
|
seasonguess.most_dnfs_id = guesses[3] # Pylance throws error but nullcheck is done
|
||||||
seasonguess.most_gained_id = guesses[4]
|
seasonguess.most_gained_id = guesses[4] # Pylance throws error but nullcheck is done
|
||||||
seasonguess.most_lost_id = guesses[5]
|
seasonguess.most_lost_id = guesses[5] # Pylance throws error but nullcheck is done
|
||||||
seasonguess.team_winners_id = teamwinners.id
|
seasonguess.team_winners_id = teamwinners.id # Pylance throws error but nullcheck is done
|
||||||
seasonguess.podium_drivers_id = podiumdrivers.id
|
seasonguess.podium_drivers_id = podiumdrivers.id # Pylance throws error but nullcheck is done
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return redirect("/season")
|
return redirect("/season")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/enter")
|
@app.route("/enter")
|
||||||
def enterraceresult():
|
def entercurrentraceresult():
|
||||||
return render_template("enter.jinja")
|
return redirect("/enter/Current")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/enter/<resultname>")
|
||||||
|
def enterraceresult(resultname: str):
|
||||||
|
drivers: List[Driver] = Driver.query.all()
|
||||||
|
raceresults: List[RaceResult] = RaceResult.query.all()[::-1]
|
||||||
|
|
||||||
|
# Find next race without result
|
||||||
|
nextid = raceresults[0].race_id + 1 if len(raceresults) > 0 else 1
|
||||||
|
nextrace: Race | None = Race.query.filter_by(id=nextid).first()
|
||||||
|
|
||||||
|
activeresult: RaceResult | None = None
|
||||||
|
if resultname != "Current":
|
||||||
|
# Obtain the chosen result
|
||||||
|
activeresult = list(filter(lambda result: result.race.grandprix == resultname, raceresults))[0]
|
||||||
|
else:
|
||||||
|
# Obtain the current result if it exists
|
||||||
|
activeresult = raceresults[0] if len(raceresults) > 0 and raceresults[0].race_id == nextrace else None
|
||||||
|
|
||||||
|
# Remove NONE driver
|
||||||
|
drivers = [driver for driver in drivers if driver.name != "NONE"]
|
||||||
|
|
||||||
|
return render_template("enter.jinja",
|
||||||
|
drivers=drivers,
|
||||||
|
race=nextrace,
|
||||||
|
results=raceresults,
|
||||||
|
activeresult=activeresult)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/enterresult/<raceid>", methods=["POST"])
|
||||||
|
def enterresult(raceid):
|
||||||
|
pxxs: List[str] = request.form.getlist("pxxdrivers")
|
||||||
|
dnfs: List[str] = request.form.getlist("dnf-drivers")
|
||||||
|
excludes: List[str] = request.form.getlist("exclude-drivers")
|
||||||
|
|
||||||
|
# Use strings as keys, as these dicts will be serialized to json
|
||||||
|
pxxs_dict: Dict[str, str] = {str(position + 1): driver for position, driver in enumerate(pxxs)}
|
||||||
|
dnfs_dict: Dict[str, str] = {str(position + 1): driver for position, driver in enumerate(pxxs) if driver in dnfs}
|
||||||
|
|
||||||
|
print(pxxs_dict)
|
||||||
|
|
||||||
|
raceresult: RaceResult | None = RaceResult.query.filter_by(race_id=raceid).first()
|
||||||
|
|
||||||
|
if raceresult is None:
|
||||||
|
raceresult = RaceResult()
|
||||||
|
db.session.add(raceresult)
|
||||||
|
|
||||||
|
raceresult.race_id = raceid
|
||||||
|
raceresult.pxx_ids = pxxs_dict
|
||||||
|
raceresult.dnf_ids = dnfs_dict if len(dnfs) > 0 else {"20": "NONE"}
|
||||||
|
raceresult.exclude_ids = excludes
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
race: Race | None = Race.query.filter_by(id=raceid).first()
|
||||||
|
if race is None:
|
||||||
|
print("Error: Can't redirect to /enter/<GrandPrix> because race couldn't be found")
|
||||||
|
return redirect("/enter")
|
||||||
|
|
||||||
|
return redirect(f"/enter/{race.grandprix}")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/users")
|
@app.route("/users")
|
||||||
def manageusers():
|
def manageusers():
|
||||||
users = User.query.all()
|
users: List[User] = User.query.all()
|
||||||
|
|
||||||
return render_template("users.jinja",
|
return render_template("users.jinja",
|
||||||
users=users)
|
users=users)
|
||||||
@ -262,7 +297,11 @@ def manageusers():
|
|||||||
|
|
||||||
@app.route("/adduser", methods=["POST"])
|
@app.route("/adduser", methods=["POST"])
|
||||||
def adduser():
|
def adduser():
|
||||||
username = request.form.get("select-add-user").strip()
|
username: str | None = request.form.get("select-add-user")
|
||||||
|
|
||||||
|
if username is None or len(username) == 0:
|
||||||
|
print(f"Not adding user, since no username was received")
|
||||||
|
return redirect("/users")
|
||||||
|
|
||||||
if len(User.query.filter_by(name=username).all()) > 0:
|
if len(User.query.filter_by(name=username).all()) > 0:
|
||||||
print(f"Not adding user {username}: Already exists!")
|
print(f"Not adding user {username}: Already exists!")
|
||||||
@ -280,6 +319,10 @@ def adduser():
|
|||||||
def deleteuser():
|
def deleteuser():
|
||||||
username = request.form.get("select-delete-user")
|
username = request.form.get("select-delete-user")
|
||||||
|
|
||||||
|
if username is None or len(username) == 0:
|
||||||
|
print(f"Not deleting user, since no username was received")
|
||||||
|
return redirect("/users")
|
||||||
|
|
||||||
if username == "Select User":
|
if username == "Select User":
|
||||||
return redirect("/users")
|
return redirect("/users")
|
||||||
|
|
||||||
|
|||||||
122
model.py
122
model.py
@ -1,4 +1,4 @@
|
|||||||
from typing import List
|
from typing import ClassVar, List, Dict
|
||||||
|
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, PickleType
|
from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, PickleType
|
||||||
@ -14,6 +14,10 @@ db = SQLAlchemy()
|
|||||||
|
|
||||||
|
|
||||||
class Race(db.Model):
|
class Race(db.Model):
|
||||||
|
"""
|
||||||
|
A single race at a certain date and GrandPrix in the calendar.
|
||||||
|
It stores the place to guess for this race.
|
||||||
|
"""
|
||||||
__tablename__ = "race"
|
__tablename__ = "race"
|
||||||
|
|
||||||
def from_csv(self, row):
|
def from_csv(self, row):
|
||||||
@ -32,6 +36,9 @@ class Race(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Team(db.Model):
|
class Team(db.Model):
|
||||||
|
"""
|
||||||
|
A constructor/team (name only).
|
||||||
|
"""
|
||||||
__tablename__ = "team"
|
__tablename__ = "team"
|
||||||
|
|
||||||
def from_csv(self, row):
|
def from_csv(self, row):
|
||||||
@ -42,6 +49,10 @@ class Team(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Driver(db.Model):
|
class Driver(db.Model):
|
||||||
|
"""
|
||||||
|
A F1 driver.
|
||||||
|
It stores the corresponding team + name abbreviation.
|
||||||
|
"""
|
||||||
__tablename__ = "driver"
|
__tablename__ = "driver"
|
||||||
|
|
||||||
def from_csv(self, row):
|
def from_csv(self, row):
|
||||||
@ -66,6 +77,9 @@ class Driver(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
class User(db.Model):
|
class User(db.Model):
|
||||||
|
"""
|
||||||
|
A user that can guess races (name only).
|
||||||
|
"""
|
||||||
__tablename__ = "user"
|
__tablename__ = "user"
|
||||||
__csv_header__ = ["name"]
|
__csv_header__ = ["name"]
|
||||||
|
|
||||||
@ -82,7 +96,12 @@ class User(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
class RaceResult(db.Model):
|
class RaceResult(db.Model):
|
||||||
|
"""
|
||||||
|
The result of a past race.
|
||||||
|
It stores the corresponding race and dictionaries of place-/dnf-order and a list of drivers that are excluded from the standings for this race.
|
||||||
|
"""
|
||||||
__tablename__ = "raceresult"
|
__tablename__ = "raceresult"
|
||||||
|
__allow_unmapped__ = True
|
||||||
__csv_header__ = ["id", "race_id", "pxx_ids_json", "dnf_ids_json", "exclude_ids_json"]
|
__csv_header__ = ["id", "race_id", "pxx_ids_json", "dnf_ids_json", "exclude_ids_json"]
|
||||||
|
|
||||||
def from_csv(self, row):
|
def from_csv(self, row):
|
||||||
@ -109,19 +128,19 @@ class RaceResult(db.Model):
|
|||||||
exclude_ids_json: Mapped[str] = mapped_column(String(1024))
|
exclude_ids_json: Mapped[str] = mapped_column(String(1024))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pxx_ids(self) -> List[str]:
|
def pxx_ids(self) -> Dict[str, str]:
|
||||||
return json.loads(self.pxx_ids_json)
|
return json.loads(self.pxx_ids_json)
|
||||||
|
|
||||||
@pxx_ids.setter
|
@pxx_ids.setter
|
||||||
def pxx_ids(self, new_pxx_ids: List[str]):
|
def pxx_ids(self, new_pxx_ids: Dict[str, str]):
|
||||||
self.pxx_ids_json = json.dumps(new_pxx_ids)
|
self.pxx_ids_json = json.dumps(new_pxx_ids)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dnf_ids(self) -> List[str]:
|
def dnf_ids(self) -> Dict[str, str]:
|
||||||
return json.loads(self.dnf_ids_json)
|
return json.loads(self.dnf_ids_json)
|
||||||
|
|
||||||
@dnf_ids.setter
|
@dnf_ids.setter
|
||||||
def dnf_ids(self, new_dnf_ids: List[str]):
|
def dnf_ids(self, new_dnf_ids: Dict[str, str]):
|
||||||
self.dnf_ids_json = json.dumps(new_dnf_ids)
|
self.dnf_ids_json = json.dumps(new_dnf_ids)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -134,39 +153,68 @@ class RaceResult(db.Model):
|
|||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
race: Mapped["Race"] = relationship("Race", foreign_keys=[race_id])
|
race: Mapped["Race"] = relationship("Race", foreign_keys=[race_id])
|
||||||
_pxxs = None
|
_pxxs: Dict[str, Driver] | None = None
|
||||||
_dnfs = None
|
_dnfs: Dict[str, Driver] | None = None
|
||||||
_excludes = None
|
_excludes: List[Driver] | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pxxs(self) -> List[Driver]:
|
def pxxs(self) -> Dict[str, Driver]:
|
||||||
if self._pxxs is None:
|
if self._pxxs is None:
|
||||||
self._pxxs = [
|
self._pxxs = dict()
|
||||||
driver for driver in Driver.query.all() if driver.name in self.pxx_ids
|
for position, driver_id in self.pxx_ids.items():
|
||||||
]
|
driver = Driver.query.filter_by(name=driver_id).first()
|
||||||
|
if driver is None:
|
||||||
|
raise Exception(f"Error: Couldn't find driver with id {driver_id}")
|
||||||
|
|
||||||
|
self._pxxs[position] = driver
|
||||||
|
|
||||||
return self._pxxs
|
return self._pxxs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dnfs(self) -> List[Driver]:
|
def dnfs(self) -> Dict[str, Driver]:
|
||||||
if self._dnfs is None:
|
if self._dnfs is None:
|
||||||
self._dnfs = [
|
self._dnfs = dict()
|
||||||
driver for driver in Driver.query.all() if driver.name in self.dnf_ids
|
for position, driver_id in self.dnf_ids.items():
|
||||||
]
|
driver = Driver.query.filter_by(name=driver_id).first()
|
||||||
|
if driver is None:
|
||||||
|
raise Exception(f"Error: Couldn't find driver with id {driver_id}")
|
||||||
|
|
||||||
|
self._dnfs[position] = driver
|
||||||
|
|
||||||
return self._dnfs
|
return self._dnfs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def excludes(self) -> List[Driver]:
|
def excludes(self) -> List[Driver]:
|
||||||
if self._excludes is None:
|
if self._excludes is None:
|
||||||
self._excludes = [
|
self._excludes = []
|
||||||
driver for driver in Driver.query.all() if driver.name in self.exclude_ids
|
for driver_id in self.exclude_ids:
|
||||||
]
|
driver = Driver.query.filter_by(name=driver_id).first()
|
||||||
|
if driver is None:
|
||||||
|
raise Exception(f"Error: Couldn't find driver with id {driver_id}")
|
||||||
|
|
||||||
|
self._excludes += [driver]
|
||||||
|
|
||||||
return self._excludes
|
return self._excludes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pxx(self) -> Driver:
|
||||||
|
pxx_num: str = str(self.race.pxx)
|
||||||
|
if pxx_num not in self.pxxs:
|
||||||
|
print(self.pxxs)
|
||||||
|
raise Exception(f"Error: Position {self.race.pxx} not contained in race result")
|
||||||
|
|
||||||
|
return self.pxxs[pxx_num]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dnf(self) -> Driver:
|
||||||
|
return sorted(self.dnfs.items(), reverse=True)[0][1]
|
||||||
|
|
||||||
|
|
||||||
class RaceGuess(db.Model):
|
class RaceGuess(db.Model):
|
||||||
|
"""
|
||||||
|
A guess a user made for a race.
|
||||||
|
It stores the corresponding race and the guessed drivers for PXX and DNF.
|
||||||
|
"""
|
||||||
__tablename__ = "raceguess"
|
__tablename__ = "raceguess"
|
||||||
__csv_header__ = ["id", "user_id", "race_id", "pxx_id", "dnf_id"]
|
__csv_header__ = ["id", "user_id", "race_id", "pxx_id", "dnf_id"]
|
||||||
|
|
||||||
@ -201,7 +249,11 @@ class RaceGuess(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
class TeamWinners(db.Model):
|
class TeamWinners(db.Model):
|
||||||
|
"""
|
||||||
|
A guessed list of each best driver per team.
|
||||||
|
"""
|
||||||
__tablename__ = "teamwinners"
|
__tablename__ = "teamwinners"
|
||||||
|
__allow_unmapped__ = True
|
||||||
__csv_header__ = ["id", "user_id", "winner_ids_json"]
|
__csv_header__ = ["id", "user_id", "winner_ids_json"]
|
||||||
|
|
||||||
def from_csv(self, row):
|
def from_csv(self, row):
|
||||||
@ -231,20 +283,29 @@ class TeamWinners(db.Model):
|
|||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
user: Mapped["User"] = relationship("User", foreign_keys=[user_id])
|
user: Mapped["User"] = relationship("User", foreign_keys=[user_id])
|
||||||
_winners = None
|
_winners: List[Driver] | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def winners(self) -> List[Driver]:
|
def winners(self) -> List[Driver]:
|
||||||
if self._winners is None:
|
if self._winners is None:
|
||||||
self._winners = [
|
self._winners = []
|
||||||
driver for driver in Driver.query.all() if driver.name in self.winner_ids
|
for driver_id in self.winner_ids:
|
||||||
]
|
driver = Driver.query.filter_by(name=driver_id).first()
|
||||||
|
if driver is None:
|
||||||
|
raise Exception(f"Error: Couldn't find driver with id {driver_id}")
|
||||||
|
|
||||||
|
self._winners += [driver]
|
||||||
|
|
||||||
|
|
||||||
return self._winners
|
return self._winners
|
||||||
|
|
||||||
|
|
||||||
class PodiumDrivers(db.Model):
|
class PodiumDrivers(db.Model):
|
||||||
|
"""
|
||||||
|
A guessed list of each driver that will reach at least a single podium.
|
||||||
|
"""
|
||||||
__tablename__ = "podiumdrivers"
|
__tablename__ = "podiumdrivers"
|
||||||
|
__allow_unmapped__ = True
|
||||||
__csv_header__ = ["id", "user_id", "podium_ids_json"]
|
__csv_header__ = ["id", "user_id", "podium_ids_json"]
|
||||||
|
|
||||||
def from_csv(self, row):
|
def from_csv(self, row):
|
||||||
@ -274,19 +335,26 @@ class PodiumDrivers(db.Model):
|
|||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
user: Mapped["User"] = relationship("User", foreign_keys=[user_id])
|
user: Mapped["User"] = relationship("User", foreign_keys=[user_id])
|
||||||
_podiums = None
|
_podiums: List[Driver] | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def podiums(self) -> List[Driver]:
|
def podiums(self) -> List[Driver]:
|
||||||
if self._podiums is None:
|
if self._podiums is None:
|
||||||
self._podiums = [
|
self._podiums = []
|
||||||
driver for driver in Driver.query.all() if driver.name in self.podium_ids
|
for driver_id in self.podium_ids:
|
||||||
]
|
driver = Driver.query.filter_by(name=driver_id).first()
|
||||||
|
if driver is None:
|
||||||
|
raise Exception(f"Error: Couldn't find driver with id {driver_id}")
|
||||||
|
|
||||||
|
self._podiums += [driver]
|
||||||
|
|
||||||
return self._podiums
|
return self._podiums
|
||||||
|
|
||||||
|
|
||||||
class SeasonGuess(db.Model):
|
class SeasonGuess(db.Model):
|
||||||
|
"""
|
||||||
|
A collection of bonus guesses for the entire season.
|
||||||
|
"""
|
||||||
__tablename__ = "seasonguess"
|
__tablename__ = "seasonguess"
|
||||||
__csv_header__ = ["id", "user_id",
|
__csv_header__ = ["id", "user_id",
|
||||||
"hot_take", "p2_constructor_id", "most_overtakes_id", "most_dnfs_id", "most_gained_id",
|
"hot_take", "p2_constructor_id", "most_overtakes_id", "most_dnfs_id", "most_gained_id",
|
||||||
|
|||||||
@ -95,7 +95,9 @@
|
|||||||
<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>
|
||||||
|
|
||||||
{# <script> #}
|
{% block head_extra %}{% endblock head_extra %}
|
||||||
|
|
||||||
|
{# <script defer> #}
|
||||||
{# {# Initialize Bootstrap Tooltips #} #}
|
{# {# Initialize Bootstrap Tooltips #} #}
|
||||||
{# console.log("Initializing Tooltips...") #}
|
{# console.log("Initializing Tooltips...") #}
|
||||||
{# const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') #}
|
{# const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') #}
|
||||||
@ -107,7 +109,7 @@
|
|||||||
|
|
||||||
<nav class="navbar fixed-top navbar-expand-lg bg-body-tertiary">
|
<nav class="navbar fixed-top navbar-expand-lg bg-body-tertiary">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="/">
|
<a class="navbar-brand" href="/race">
|
||||||
<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
|
||||||
|
|||||||
@ -10,14 +10,68 @@
|
|||||||
<script src="../static/script/draggable.js" defer></script>
|
<script src="../static/script/draggable.js" defer></script>
|
||||||
{% endblock head_extra %}
|
{% endblock head_extra %}
|
||||||
|
|
||||||
|
{% block navbar_center %}
|
||||||
|
{% if results | length > 0 %}
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false">
|
||||||
|
{% if activeresult is not none %}{{ activeresult.race.grandprix }}{% else %}
|
||||||
|
{{ race.grandprix }}{% endif %}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="/enter/Current">{{ race.grandprix }}</a></li>
|
||||||
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
|
{% for result in results %}
|
||||||
|
<li><a class="dropdown-item"
|
||||||
|
href="/enter/{{ result.race.grandprix }}">{{ result.race.grandprix }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock navbar_center %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<div class="card shadow-sm" style="width: 450px;">
|
<div class="card shadow-sm" style="width: 450px;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">{{ race.grandprix }}</h5>
|
<h5 class="card-title">{% if activeresult is not none %}{{ activeresult.race.grandprix }}{% else %}
|
||||||
|
{{ race.grandprix }}{% endif %}</h5>
|
||||||
|
|
||||||
<form action="/enterresult" method="post">
|
<form action="/enterresult/
|
||||||
|
{% if activeresult is not none %}{{ activeresult.race.id }}{% else %}{{ race.id }}{% endif %}"
|
||||||
|
method="post">
|
||||||
<ul id="columns" class="list-group list-group-flush">
|
<ul id="columns" class="list-group list-group-flush">
|
||||||
|
{% if activeresult is not none %}
|
||||||
|
{% for driver in activeresult.pxxs.values() %}
|
||||||
|
<li class="list-group-item column p-1" draggable="true">
|
||||||
|
{{ driver.name }}
|
||||||
|
|
||||||
|
<div class="d-inline-block float-end">
|
||||||
|
{# Driver DNFed #}
|
||||||
|
<div class="form-check form-check-reverse d-inline-block">
|
||||||
|
<input type="checkbox" class="form-check-input" value="{{ driver.name }}"
|
||||||
|
id="dnf-{{ driver.name }}" name="dnf-drivers"
|
||||||
|
{% if driver in activeresult.dnfs.values() %}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 mx-2">
|
||||||
|
<input type="checkbox" class="form-check-input" value="{{ driver.name }}"
|
||||||
|
id="exclude-{{ driver.name }}" name="exclude-drivers"
|
||||||
|
{% if driver in activeresult.excludes %}checked{% endif %}>
|
||||||
|
<label for="exclude-{{ driver.name }}" class="form-check-label text-muted">Exclude</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Standing order #}
|
||||||
|
<input type="hidden" name="pxxdrivers" value="{{ driver.name }}"></li>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
{% for driver in drivers %}
|
{% for driver in drivers %}
|
||||||
<li class="list-group-item column p-1" draggable="true">
|
<li class="list-group-item column p-1" draggable="true">
|
||||||
{{ driver.name }}
|
{{ driver.name }}
|
||||||
@ -27,7 +81,8 @@
|
|||||||
<div class="form-check form-check-reverse d-inline-block">
|
<div class="form-check form-check-reverse d-inline-block">
|
||||||
<input type="checkbox" class="form-check-input" value="{{ driver.name }}"
|
<input type="checkbox" class="form-check-input" value="{{ driver.name }}"
|
||||||
id="dnf-{{ driver.name }}" name="dnf-drivers">
|
id="dnf-{{ driver.name }}" name="dnf-drivers">
|
||||||
<label for="dnf-{{ driver.name }}" class="form-check-label text-muted">DNF</label>
|
<label for="dnf-{{ driver.name }}"
|
||||||
|
class="form-check-label text-muted">DNF</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Driver Excluded #}
|
{# Driver Excluded #}
|
||||||
@ -41,6 +96,7 @@
|
|||||||
{# Standing order #}
|
{# Standing order #}
|
||||||
<input type="hidden" name="pxxdrivers" value="{{ driver.name }}"></li>
|
<input type="hidden" name="pxxdrivers" value="{{ driver.name }}"></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<input type="submit" class="btn btn-danger mt-2" value="Save">
|
<input type="submit" class="btn btn-danger mt-2" value="Save">
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<table class="table table-bordered table-sm table-responsive">
|
<table class="table table-bordered table-sm table-responsive shadow-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" rowspan="2" class="text-center" style="width: 200px;">Race</th>
|
<th scope="col" rowspan="2" class="text-center" style="width: 200px;">Race</th>
|
||||||
@ -57,51 +57,61 @@
|
|||||||
<td> </td>
|
<td> </td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{# Next Race Guess #}
|
{# Current Result, only displayed for all users overview #}
|
||||||
{# {% if nextrace is not none %}#}
|
{% if activeuser is none %}
|
||||||
{# <tr class="table-light">#}
|
<tr class="table-danger">
|
||||||
{# <td class="text-nowrap">#}
|
<td class="text-nowrap">
|
||||||
{# <span class="fw-bold">{{ nextrace.id }}:</span> {{ nextrace.grandprix }}<br>#}
|
<span class="fw-bold">{{ currentrace.id }}:</span> {{ currentrace.grandprix }}<br>
|
||||||
{# <small><span class="fw-bold">Guess:</span> P{{ nextrace.pxx }}</small>#}
|
<small><span class="fw-bold">Guess:</span> P{{ currentrace.pxx }}</small>
|
||||||
{# </td>#}
|
</td>
|
||||||
{##}
|
|
||||||
{# {% for user in users %}#}
|
|
||||||
{# <td>#}
|
|
||||||
{# <form action="/guessrace/{{ nextrace.id }}/{{ user.name }}" method="post">#}
|
|
||||||
{# Driver PXX Select #}
|
|
||||||
{# {{ driver_select_with_preselect(currentselection.get(user.name).pxx.abbr if user.name in currentselection else "",#}
|
|
||||||
{# "pxxselect", "P" ~ nextrace.pxx ~ ":") }}#}
|
|
||||||
{##}
|
|
||||||
{# Driver DNF Select #}
|
|
||||||
{# <div class="mt-2">#}
|
|
||||||
{# {{ driver_select_with_preselect(currentselection.get(user.name).dnf.abbr if user.name in currentselection else "",#}
|
|
||||||
{# "dnfselect", "DNF:") }}#}
|
|
||||||
{# </div>#}
|
|
||||||
{##}
|
|
||||||
{# <input type="submit" class="btn btn-danger mt-2 w-100" value="Save">#}
|
|
||||||
{# </form>#}
|
|
||||||
{# </td>#}
|
|
||||||
{# {% endfor %}#}
|
|
||||||
{##}
|
|
||||||
{# Enter Race Result #}
|
|
||||||
{# <td> </td>#}
|
|
||||||
{# <td>#}
|
|
||||||
{# <form action="/enterresult/{{ nextrace.id }}" method="post">#}
|
|
||||||
{# Driver PXX Select#}
|
|
||||||
{# {{ driver_select("pxxselect", "P" ~ nextrace.pxx ~ ":") }}#}
|
|
||||||
{##}
|
|
||||||
{# Driver DNF Select#}
|
|
||||||
{# <div class="mt-2">#}
|
|
||||||
{# {{ driver_select("dnfselect", "DNF:") }}#}
|
|
||||||
{# </div>#}
|
|
||||||
{##}
|
|
||||||
{# <input type="submit" class="btn btn-danger mt-2 w-100" value="Save">#}
|
|
||||||
{# </form>#}
|
|
||||||
{# </td>#}
|
|
||||||
{# </tr>#}
|
|
||||||
{# {% endif %}#}
|
|
||||||
|
|
||||||
{# Race Results #}
|
{% for user in users %}
|
||||||
|
<td class="text-center text-nowrap">
|
||||||
|
{% if (currentrace.id in guesses) and (user.name in guesses.get(currentrace.id)) %}
|
||||||
|
{% set pxx = guesses.get(currentrace.id).get(user.name).pxx.abbr %}
|
||||||
|
{% set dnf = guesses.get(currentrace.id).get(user.name).dnf.abbr %}
|
||||||
|
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item" style="background-color: inherit;">P{{ currentrace.pxx }}: {{ pxx }}</li>
|
||||||
|
<li class="list-group-item" style="background-color: inherit;">DNF: {{ dnf }}</li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Enter Guess, only displayed for single user focused view #}
|
||||||
|
{% if activeuser is not none %}
|
||||||
|
<tr class="table-danger">
|
||||||
|
<td class="text-nowrap">
|
||||||
|
<span class="fw-bold">{{ currentrace.id }}:</span> {{ currentrace.grandprix }}<br>
|
||||||
|
<small><span class="fw-bold">Guess:</span> P{{ currentrace.pxx }}</small>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<form action="/guessrace/{{ currentrace.id }}/{{ activeuser.name }}" method="post">
|
||||||
|
{# Driver PXX Select #}
|
||||||
|
{{ driver_select_with_preselect(guesses.get(currentrace.id).get(activeuser.name).pxx.abbr if (currentrace.id in guesses and activeuser.name in guesses.get(currentrace.id)) else "",
|
||||||
|
"pxxselect", "P" ~ currentrace.pxx ~ ":") }}
|
||||||
|
|
||||||
|
{# Driver DNF Select #}
|
||||||
|
<div class="mt-2">
|
||||||
|
{{ driver_select_with_preselect(guesses.get(currentrace.id).get(activeuser.name).dnf.abbr if (currentrace.id in guesses and activeuser.name in guesses.get(currentrace.id)) else "",
|
||||||
|
"dnfselect", "DNF:") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save">
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Past Race Results #}
|
||||||
{% for raceresult in raceresults %}
|
{% for raceresult in raceresults %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
|
|||||||
Reference in New Issue
Block a user