From 729203409fb73fdbac19b8af9d647883a5ac96b0 Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Mon, 14 Jul 2025 12:25:55 +0200 Subject: Back to working --- app/config.py | 5 +- app/controller.py | 173 ++++++++++++++++++++++++++++----------- app/model.py | 207 ++++++++--------------------------------------- app/routes.py | 34 +++++++- app/static/custom.js | 10 ++- app/templates/games.j2 | 12 +-- app/templates/players.j2 | 17 ++++ app/utilities.py | 68 +++++++++++++--- 8 files changed, 286 insertions(+), 240 deletions(-) diff --git a/app/config.py b/app/config.py index fa16e1f..d7509a1 100644 --- a/app/config.py +++ b/app/config.py @@ -4,10 +4,11 @@ from os.path import abspath, dirname, join class Config: SECRET_KEY = environ.get("SECRET_KEY") or None - DATABASE_CSV = environ.get("DATABASE_CSV") or None SQLALCHEMY_DATABASE_URI = "sqlite:///" + join(abspath(dirname(__file__)), "app.db") COURT_COUNT = 12 PLAYERS_PER_COURT = 4 - GUEST_LEVELS = [1, 3, 5] MIN_PLAYER_LEVEL = 1 MAX_PLAYER_LEVEL = 10 + DEFAULTS = {"---": 1, "RESERVED": 2} + GUEST_COUNT = 4 + GUESTS = {"Beginner": 1, "Novice": 3, "Intermediate": 5} \ No newline at end of file diff --git a/app/controller.py b/app/controller.py index d8a178f..1c2c5bf 100644 --- a/app/controller.py +++ b/app/controller.py @@ -1,15 +1,14 @@ from math import floor from app import app, db from datetime import date +from typing import Sequence +from pandas import DataFrame from operator import attrgetter -from sqlalchemy import func, Date +from sqlalchemy import func, select, not_ from app.model import ( Player, - Players, - Courts, + Court, Game, - DisplayPlayer, - DisplayTeam, DisplayGame, ) from app.utilities import ( @@ -17,9 +16,103 @@ from app.utilities import ( get_team_games, get_shuffle_games, separate_teams_and_players, + initialize_random_database, + initialize_default_players, ) +class Courts: + + def __init__(self): + with app.app_context(): + courts = db.session.query(Court).all() + if len(courts) == 0: + for _ in range(app.config["COURT_COUNT"]): + db.session.add(Court()) + db.session.commit() + + def toggle(self, id: int) -> bool: + court = db.session.get(Court, id) + if court is None: + raise ValueError(f"Court {id} doesn't exist.") + new_status = not court.status + court.status = new_status + db.session.commit() + return new_status + + def active(self): + return db.session.query(Court).where(Court.status).all() + + def inactive(self): + return db.session.query(Court).where(not_(Court.status)).all() + + def all(self) -> list[Court]: + return db.session.query(Court).order_by(Court.id).all() + + +class Players: + + def __init__(self) -> None: + with app.app_context(): + if len(self.regulars()) == 0: + initialize_random_database() + + def update(self, data: DataFrame): + initialize_default_players() + for entry in data.to_dict("records"): + args = entry | {"status": False} + db.session.add(Player(**args)) # type: ignore + db.session.commit() + + def get(self, id: int) -> bool: + player = db.session.get(Player, id) + if player is None: + raise ValueError(f"User {id} doesn't exist.") + return player.status + + def toggle(self, id: int) -> Player: + player = db.session.get(Player, id) + if player is None: + raise ValueError(f"User {id} doesn't exist.") + player.status = not player.status + db.session.commit() + return player + + def regulars(self) -> list[Player]: + filter_list = list(app.config["GUESTS"].keys()) + list( + app.config["DEFAULTS"].keys() + ) + return ( + db.session.query(Player) + .where(Player.id > 1) + .where(Player.first_name.not_in(filter_list)) + .order_by(Player.first_name, Player.last_name) + .all() + ) + + def guests(self) -> list[Player]: + return ( + db.session.query(Player) + .where(Player.id > 1) + .where(Player.first_name.in_(app.config["GUESTS"].keys())) + .order_by(Player.first_name, Player.last_name) + .all() + ) + + def active(self) -> Sequence[Player]: + query = ( + select(Player) + .where(Player.status) + .order_by(Player.first_name, Player.last_name) + ) + return db.session.scalars(query).all() + + def reset(self): + for player in db.session.scalars(select(Player)): + player.status = False + db.session.commit() + + class Session: levels: int = 10 teams: bool = False @@ -27,27 +120,33 @@ class Session: courts: Courts = Courts() players: Players = Players() + def _today(self): + games = ( + db.session.query(Game).filter(func.date(Game.date) == date.today()).all() + ) + return games + def __init__(self): with app.app_context(): - games = db.session.query(Game) - db.session.query(Game).delete() - id = self.players.default_id() - player_data = {f"player_{i+1}": id for i in range(4)} - for court in self.courts.all(): - args = {"round_id": 0, "court_id": court.id} | player_data - db.session.add(Game(**args)) - db.session.commit() + if len(self._today()) == 0: + query = select(Player).where(Player.first_name == "---") + player = db.session.scalars(query).first() + if player is None: + raise ValueError(f"Database isn't initialized.") + player_data = {f"player_{i+1}": player.id for i in range(4)} + for court in self.courts.all(): + args = {"round_id": 0, "court_id": court.id} | player_data + db.session.add(Game(**args)) + db.session.commit() def round(self) -> int: - games = ( - db.session.query(Game).filter(func.date(Game.date) == date.today()).all() - ) + games = self._today() round_id = max([game.round_id for game in games]) if round_id is None: raise ValueError(f"Database isn't initialized") return round_id - def all(self) -> list[DisplayGame]: + def current_round(self) -> list[DisplayGame]: games = ( db.session.query(Game) .filter(func.date(Game.date) == date.today()) @@ -55,22 +154,11 @@ class Session: .order_by(Game.court_id) .all() ) - dg = [] - for game in games: - dp = [] - for i in range(4): - p = db.session.get(Player, getattr(game, f"player_{i+1}")) - if p is None: - raise ValueError(f"Player id mismatch.") - dp.append(p) - t1 = DisplayTeam(DisplayPlayer(dp[0]), DisplayPlayer(dp[1])) - t2 = DisplayTeam(DisplayPlayer(dp[2]), DisplayPlayer(dp[3])) - db.session.get(Player, game.player_2) - dg.append(DisplayGame(game.court_id, t1, t2)) - return dg + games = [DisplayGame(game) for game in games] + return games def propose(self) -> list[DisplayGame]: - reserved_courts = [court for court in self.courts.all() if not court.status] + reserved_courts = self.courts.inactive() reserved_games = get_placeholder_games(reserved_courts, "RESERVED") active_courts = self.courts.active() active_players = self.players.active() @@ -87,30 +175,21 @@ class Session: team_games = get_team_games(team_courts, active_teams) shuffle_games = get_shuffle_games(shuffle_courts, active_players, self.levels) self.proposal = shuffle_games + team_games + blank_games + reserved_games - self.proposal.sort(key=attrgetter("court_id")) + self.proposal.sort(key=attrgetter("court.id")) return self.proposal def confirm(self) -> list[DisplayGame]: if len(self.proposal) == 0: - return self.all() - round_number = self.round() + 1 + return self.current_round() + round_id = self.round() + 1 for game in self.proposal: - db.session.add( - Game( - round_number, - game.court_id, - game.team_1.player_1.id, - game.team_1.player_2.id, - game.team_2.player_1.id, - game.team_2.player_2.id, - ) - ) + db.session.add(game.to_game(round_id)) db.session.commit() self.proposal = [] - return self.all() + return self.current_round() def reset(self) -> list[DisplayGame]: - db.session.query(Game).filter(Game.date.cast(Date) == date.today()).delete() + db.session.query(Game).filter(func.date(Game.date) == date.today()).delete() db.session.commit() self.__init__() - return self.all() + return self.current_round() diff --git a/app/model.py b/app/model.py index 2f8c609..3ec663e 100644 --- a/app/model.py +++ b/app/model.py @@ -1,14 +1,9 @@ -from math import floor from app import app, db -from pandas import read_csv -from dataclasses import dataclass from sqlalchemy.orm import Mapped from datetime import date, datetime -from random import randint, getrandbits -from typing import Tuple, Optional, Sequence +from typing import Optional, Annotated from sqlalchemy.orm import mapped_column as mc -from names import get_first_name, get_last_name -from sqlalchemy import String, Boolean, Integer, ForeignKey, select +from sqlalchemy import String, Boolean, Integer, ForeignKey class User(db.Model): @@ -28,48 +23,10 @@ class Court(db.Model): return f"Court {self.id} is {"active" if self.status else "inactive"}" -class Courts: - - def __init__(self): - with app.app_context(): - if app.config["COURT_COUNT"] is not None: - db.session.query(Court).delete() - for i in range(int(app.config["COURT_COUNT"])): - db.session.add(Court()) - db.session.commit() - - def get(self, id: int) -> bool: - court = db.session.get(Court, id) - if court is None: - raise ValueError(f"Court {id} doesn't exist.") - return court.status - - def toggle(self, id: int) -> bool: - court = db.session.get(Court, id) - if court is None: - raise ValueError(f"Court {id} doesn't exist.") - new_status = not court.status - court.status = new_status - db.session.commit() - return new_status - - def active(self): - return db.session.query(Court).where(Court.status).all() - - def all(self) -> list[Court]: - courts = [court for court in db.session.query(Court).order_by(Court.id)] - return courts - - def reset(self): - for court in db.session.scalars(select(Court)): - court.status = True - db.session.commit() - - class Player(db.Model): id: Mapped[int] = mc(primary_key=True) - first_name: Mapped[str] = mc(String(64), index=True, default="---") - last_name: Mapped[str] = mc(String(64), index=True, default="---") + first_name: Mapped[str] = mc(String(64), index=True) + last_name: Mapped[str] = mc(String(64), index=True) team: Mapped[int] = mc(Integer, index=True) status: Mapped[bool] = mc(Boolean) level: Mapped[int] = mc(Integer) @@ -94,111 +51,6 @@ class Player(db.Model): return f"{self.first_name} {self.last_name} is {"active" if self.status else "inactive"}" -def initialize_random_database( - player_count: int = 80, guest_per_level: int = 4 -) -> None: - db.session.add(Player(**{"id": 0, "first_name": "---", "last_name": ""})) - db.session.add(Player(**{"id": 1, "first_name": "RESERVED", "last_name": ""})) - total_teams = 5 - players_per_team = 5 - for i in range(player_count): - if i < total_teams * players_per_team: - team = floor(i / players_per_team) + 1 - else: - team = 0 - args = { - "first_name": get_first_name(), - "last_name": get_last_name(), - "team": team, - "status": bool(getrandbits(1)), - "level": randint( - app.config["MIN_PLAYER_LEVEL"], app.config["MAX_PLAYER_LEVEL"] - ), - } - db.session.add(Player(**args)) - meta_data = {"first_name": "Guest", "team": 0, "status": False} - for level_id, level in enumerate(app.config["GUEST_LEVELS"]): - for player_id in range(guest_per_level): - unique_id = (level_id * guest_per_level) + player_id + 1 - args = meta_data | {"last_name": str(unique_id), "level": level} - db.session.add(Player(**args)) - db.session.commit() - - -class Players: - - def __init__(self) -> None: - with app.app_context(): - if app.config["DATABASE_CSV"] is not None: - data = read_csv(app.config["DATABASE_CSV"]) - db.session.query(Player).delete() - db.session.add( - Player(**{"id": 0, "first_name": "---", "last_name": ""}) - ) - db.session.add( - Player(**{"id": 1, "first_name": "RESERVED", "last_name": ""}) - ) - for entry in data.to_dict("records"): - args = entry | {"status": False} - db.session.add(Player(**args)) # type: ignore - db.session.commit() - else: - if len(self.all()) == 0: - initialize_random_database() - - def default_id(self): - query = select(Player).where(Player.first_name == "---") - player = db.session.scalars(query).first() - if player is None: - raise ValueError(f"Database isn't initialized.") - return player.id - - def reserved_id(self): - query = select(Player).where(Player.first_name == "RESERVED") - player = db.session.scalars(query).first() - if player is None: - raise ValueError(f"Database isn't initialized.") - return player.id - - def get(self, id: int) -> bool: - player = db.session.get(Player, id) - if player is None: - raise ValueError(f"User {id} doesn't exist.") - return player.status - - def toggle(self, id: int) -> Player: - player = db.session.get(Player, id) - if player is None: - raise ValueError(f"User {id} doesn't exist.") - player.status = not player.status - db.session.commit() - return player - - def all(self) -> Tuple[list[Player], list[Player]]: - players = ( - db.session.query(Player) - .where(Player.id > 1) - .order_by(Player.first_name, Player.last_name) - ) - regulars = [p for p in players if p.first_name != "Guest"] - guests = [p for p in players if p.first_name == "Guest"] - guests.sort(key=lambda x: int(x.last_name)) - return regulars, guests - - def active(self) -> Sequence[Player]: - query = ( - select(Player) - .where(Player.status) - .order_by(Player.first_name, Player.last_name) - ) - return db.session.scalars(query).all() - - def reset(self): - for player in db.session.scalars(select(Player)): - player.status = False - db.session.commit() - - class Game(db.Model): id: Mapped[int] = mc(primary_key=True) round_id: Mapped[int] = mc(Integer, index=True) @@ -211,14 +63,15 @@ class Game(db.Model): def __init__( self, - round_id: int, + round_id: Optional[int], court_id: int, player_1: int, player_2: int, player_3: int, player_4: int, ): - self.round_id = round_id + if round_id is not None: + self.round_id = round_id self.court_id = court_id self.player_1 = player_1 self.player_2 = player_2 @@ -226,22 +79,32 @@ class Game(db.Model): self.player_4 = player_4 -class DisplayPlayer: - - def __init__(self, player: Player): - self.id = player.id - self.first_name = player.first_name - self.last_name = player.last_name - - -@dataclass -class DisplayTeam: - player_1: DisplayPlayer - player_2: DisplayPlayer - - -@dataclass class DisplayGame: - court_id: int - team_1: DisplayTeam - team_2: DisplayTeam + court: Court + team_1: Annotated[list[Player], 2] + team_2: Annotated[list[Player], 2] + + def __init__(self, game: Game): + court = db.session.get(Court, game.court_id) + if court is None: + raise ValueError(f"Unknown court") + self.court = court + players = [] + for i in range(4): + player_id = getattr(game, f"player_{i+1}") + p = db.session.get(Player, player_id) + if p is None: + raise ValueError(f"Player ID: {player_id} mismatch.") + players.append(p) + self.team_1 = players[:2] + self.team_2 = players[2:] + + def to_game(self, round_id) -> Game: + return Game( + round_id, + self.court.id, + self.team_1[0].id, + self.team_1[1].id, + self.team_2[0].id, + self.team_2[1].id, + ) diff --git a/app/routes.py b/app/routes.py index cffbae9..ce9b714 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,6 +1,8 @@ from app import app +from pandas import read_csv from app.forms import LoginForm from app.controller import Session +from werkzeug.utils import secure_filename from flask import ( render_template, request, @@ -48,7 +50,9 @@ def confirm_games(): @app.route("/clear", methods=["POST"]) def clear_games(): - return render_template("games.j2", games=g.all(), title=f"Round: {g.round()}") + return render_template( + "games.j2", games=g.current_round(), title=f"Round: {g.round()}" + ) @app.route("/reset", methods=["POST"]) @@ -66,9 +70,11 @@ def toggle_player(player_request): @app.route("/players", methods=["GET"]) def get_list_of_players(): - regulars, guests = g.players.all() return render_template( - "players.j2", regulars=regulars, guests=guests, url_for=url_for + "players.j2", + regulars=g.players.regulars(), + guests=g.players.guests(), + url_for=url_for, ) @@ -79,6 +85,28 @@ def reset_all_players(): response.headers["HX-Refresh"] = "true" return response +@app.route("/update_players", methods=["POST"]) +def update_all_players(): + file = request.files.get("players") + response = Response("File name or data has issues!", 400) + response.headers["HX-Refresh"] = "true" + if file is None: + return response + elif file.filename is None: + return response + file_name = secure_filename(file.filename) + try: + data = read_csv(file_name) + required_columns = ["id","first_name","last_name","level","team"] + if not all([item in data.columns for item in required_columns]): + raise ImportError(f"All columns aren't available in the data.") + except: + return response + g.players.update(data) + response = Response("Players updated!") + response.headers["HX-Refresh"] = "true" + return response + @app.route("/court-toggle/", methods=["POST"]) def toggle_court(court_number): diff --git a/app/static/custom.js b/app/static/custom.js index 2e4b049..47f64ca 100644 --- a/app/static/custom.js +++ b/app/static/custom.js @@ -84,4 +84,12 @@ document.getElementById("timer-dec").addEventListener('click', ()=> { minsPerRound -= 1; resetTimer(); } -}); \ No newline at end of file +}); + +const fileInput = document.querySelector("#file-js input[type=file]"); + fileInput.onchange = () => { + if (fileInput.files.length > 0) { + const fileName = document.querySelector("#file-js .file-name"); + fileName.textContent = fileInput.files[0].name; + } +}; \ No newline at end of file diff --git a/app/templates/games.j2 b/app/templates/games.j2 index ccfd395..dff1e0f 100644 --- a/app/templates/games.j2 +++ b/app/templates/games.j2 @@ -9,12 +9,12 @@ {% for game in games %} - - {{ game.court_id }} - {{ game.team_1.player_1.first_name }}{{ " " + game.team_1.player_1.last_name }} - {{ game.team_1.player_2.first_name }}{{ " " + game.team_1.player_2.last_name }} - {{ game.team_2.player_1.first_name }}{{ " " + game.team_2.player_1.last_name }} - {{ game.team_2.player_2.first_name }}{{ " " + game.team_2.player_2.last_name }} + + {{ game.court.id }} + {{ game.team_1[0].first_name }}{{ " " + game.team_1[0].last_name }} + {{ game.team_1[1].first_name }}{{ " " + game.team_1[1].last_name }} + {{ game.team_2[0].first_name }}{{ " " + game.team_2[0].last_name }} + {{ game.team_2[1].first_name }}{{ " " + game.team_2[1].last_name }} {% endfor %} diff --git a/app/templates/players.j2 b/app/templates/players.j2 index 0beeee9..16e3878 100644 --- a/app/templates/players.j2 +++ b/app/templates/players.j2 @@ -25,6 +25,23 @@ +
+
+
+ + +
+
+
{{ footer() }} diff --git a/app/utilities.py b/app/utilities.py index 133f9e1..cd75d2a 100644 --- a/app/utilities.py +++ b/app/utilities.py @@ -1,9 +1,12 @@ +from math import floor from app import app, db from datetime import date from sqlalchemy import func from typing import Tuple, Sequence from random import shuffle, sample -from app.model import Player, DisplayPlayer, Court, Game, DisplayGame, DisplayTeam +from random import randint, getrandbits +from names import get_first_name, get_last_name +from app.model import Player, Court, Game, DisplayGame def _shuffle_two_lists(a: list, b: list) -> Tuple[list, list]: @@ -87,8 +90,10 @@ def get_placeholder_games(courts: list[Court], name: str) -> list[DisplayGame]: p = db.session.query(Player).where(Player.first_name == name).first() if p is None: raise ValueError(f"Database isn't initialized.") - dummy_team = DisplayTeam(DisplayPlayer(p), DisplayPlayer(p)) - games = [DisplayGame(court.id, dummy_team, dummy_team) for court in courts] + player_data = {"round_id": None} | {f"player_{i+1}": p.id for i in range(4)} + games = [ + DisplayGame(Game(**({"court_id": court.id} | player_data))) for court in courts # type: ignore + ] return games @@ -99,10 +104,10 @@ def get_team_games( for court, (_, players) in zip(courts, teams.items()): players = sample(players, app.config["PLAYERS_PER_COURT"]) players.sort(key=lambda x: x.level, reverse=True) - dp = [DisplayPlayer(p) for p in players] - games.append( - DisplayGame(court.id, DisplayTeam(dp[0], dp[3]), DisplayTeam(dp[1], dp[2])) + game = Game( + None, court.id, players[0].id, players[3].id, players[1].id, players[2].id ) + games.append(DisplayGame(game)) return games @@ -114,8 +119,53 @@ def get_shuffle_games( court_players = group_by_four(selected_players, total_levels) games = [] for court, players in zip(courts, court_players): - dp = [DisplayPlayer(p) for p in players] - games.append( - DisplayGame(court.id, DisplayTeam(dp[0], dp[3]), DisplayTeam(dp[1], dp[2])) + game = Game( + None, court.id, players[0].id, players[3].id, players[1].id, players[2].id ) + games.append(DisplayGame(game)) return games + + +def initialize_default_players(): + db.session.query(Player).delete() + for k, v in app.config["DEFAULTS"].items(): + db.session.add(Player(**{"id": v, "first_name": k, "last_name": ""})) + guest_offset = 10000 + for i, (k, v) in enumerate(app.config["GUESTS"].items()): + level_offset = app.config["GUEST_COUNT"] * i + for j in range(app.config["GUEST_COUNT"]): + db.session.add( + Player( + **{ + "id": guest_offset + level_offset + j, + "first_name": k, + "last_name": f"{j + 1}", + "level": v, + } + ) + ) + db.session.commit() + + +def initialize_random_database(): + initialize_default_players() + player_count = 80 + total_teams = 5 + players_per_team = 5 + for i in range(player_count): + if i < total_teams * players_per_team: + team = floor(i / players_per_team) + 1 + else: + team = 0 + args = { + "id": 3 + i, + "first_name": get_first_name(), + "last_name": get_last_name(), + "team": team, + "status": bool(getrandbits(1)), + "level": randint( + app.config["MIN_PLAYER_LEVEL"], app.config["MAX_PLAYER_LEVEL"] + ), + } + db.session.add(Player(**args)) + db.session.commit() -- cgit v1.3.1