From c773faeea93fe5db2eb493c79c0c82f46336493c Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Sun, 6 Jul 2025 13:31:37 +0200 Subject: Running application --- app/templates/controls.j2 | 47 ++++++++++++++++++++++ app/templates/games.j2 | 21 ++++++++++ app/templates/index.j2 | 13 +++++++ app/templates/macros.j2 | 99 +++++++++++++++++++++++++++++++++++++++++++++++ app/templates/players.j2 | 31 +++++++++++++++ 5 files changed, 211 insertions(+) create mode 100644 app/templates/controls.j2 create mode 100644 app/templates/games.j2 create mode 100644 app/templates/index.j2 create mode 100644 app/templates/macros.j2 create mode 100644 app/templates/players.j2 (limited to 'app/templates') diff --git a/app/templates/controls.j2 b/app/templates/controls.j2 new file mode 100644 index 0000000..0183c97 --- /dev/null +++ b/app/templates/controls.j2 @@ -0,0 +1,47 @@ +{% from "macros.j2" import court_button %} +
+
+ {% for key, value in courts.items() %} + {{ court_button(key, value) }} + {% endfor %} +
+
+
+
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/app/templates/games.j2 b/app/templates/games.j2 new file mode 100644 index 0000000..46150f7 --- /dev/null +++ b/app/templates/games.j2 @@ -0,0 +1,21 @@ + + + + + + + + + + +{% for game in games %} + + + +{% endfor %} + +
{{ title }}
Court Team 1 Team 2
{{ game.court }}{{ game.team1.player1.first }}{{ " " + game.team1.player1.last }} + {{ game.team1.player2.first }}{{ " " + game.team1.player2.last }} + {{ game.team2.player1.first }}{{ " " + game.team2.player1.last }} + {{ game.team2.player2.first }}{{ " " + game.team2.player2.last }} +
\ No newline at end of file diff --git a/app/templates/index.j2 b/app/templates/index.j2 new file mode 100644 index 0000000..ce0d531 --- /dev/null +++ b/app/templates/index.j2 @@ -0,0 +1,13 @@ +{% from "macros.j2" import header, footer, navbar %} + + + {{ header(url_for) }} + +
+ {{ navbar(url_for) }} +
+
+ {{ footer() }} +
+ + diff --git a/app/templates/macros.j2 b/app/templates/macros.j2 new file mode 100644 index 0000000..2974744 --- /dev/null +++ b/app/templates/macros.j2 @@ -0,0 +1,99 @@ +{% macro court_button(id, status) -%} + +{%- endmacro %} + +{% macro player_button(player) -%} + +{%- endmacro %} + +{% macro guest_control(level, value) -%} +
+
+ +
+
+ +
+
+ +
+
+{%- endmacro %} + +{% macro header(url_for) -%} + + + + Match Up! + + + + + + + + + +{%- endmacro %} + +{% macro footer() -%} +
Made by Karan using Flask, HTMX, Notyf and Bulma
+{%- endmacro %} + +{% macro navbar(url_for) -%} + +{%- endmacro %} \ No newline at end of file diff --git a/app/templates/players.j2 b/app/templates/players.j2 new file mode 100644 index 0000000..0beeee9 --- /dev/null +++ b/app/templates/players.j2 @@ -0,0 +1,31 @@ +{% from "macros.j2" import header, navbar, footer, player_button, guest_control %} + + + {{ header(url_for) }} + +
+ {{ navbar(url_for) }} +

Members

+
+
+ {% for regular in regulars %} +
{{ player_button(regular) }}
+ {% endfor %} +
+

Guests

+
+
+ {% for guest in guests %} +
{{ player_button(guest) }}
+ {% endfor %} +
+
+
+
+ +
+ {{ footer() }} +
+ + \ No newline at end of file -- cgit v1.3.1 From ceb264b6bdfed8c51e54deed2b6132fecff3934d Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Sun, 6 Jul 2025 15:13:27 +0200 Subject: Added the basic login --- app/__init__.py | 2 ++ app/config.py | 4 ++++ app/forms.py | 9 +++++++++ app/routes.py | 10 +++++++++- app/templates/index.j2 | 9 +++++++++ app/templates/login.j2 | 25 +++++++++++++++++++++++++ 6 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 app/config.py create mode 100644 app/forms.py create mode 100644 app/templates/login.j2 (limited to 'app/templates') diff --git a/app/__init__.py b/app/__init__.py index 96c8ef5..7b0c03d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,5 +1,7 @@ from flask import Flask +from app.config import Config app = Flask(__name__) +app.config.from_object(Config) from app import routes diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..aeb2840 --- /dev/null +++ b/app/config.py @@ -0,0 +1,4 @@ +from os import environ + +class Config: + SECRET_KEY = environ.get("SECRET_KEY") or "you-will-never-guess" \ No newline at end of file diff --git a/app/forms.py b/app/forms.py new file mode 100644 index 0000000..504824a --- /dev/null +++ b/app/forms.py @@ -0,0 +1,9 @@ +from flask_wtf import FlaskForm +from wtforms.validators import DataRequired +from wtforms import StringField, PasswordField, BooleanField, SubmitField + +class LoginForm(FlaskForm): + username = StringField("Username", validators=[DataRequired()]) + password = PasswordField("Password", validators=[DataRequired()]) + remember_me = BooleanField("Remember Me") + submit = SubmitField("Sign In") \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index c18d93b..b943e16 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,6 +1,7 @@ from app import app +from app.forms import LoginForm from app.controller import RoundGenerator -from flask import render_template, request, url_for, Response +from flask import render_template, request, url_for, Response, flash, redirect rg = RoundGenerator() @@ -8,6 +9,13 @@ rg = RoundGenerator() def home(): return render_template("index.j2", url_for=url_for) +@app.route("/login", methods=["GET", "POST"]) +def login(): + form = LoginForm() + if form.validate_on_submit(): + flash(f"User {form.username} is requesting to login.") + return redirect("/") + return render_template("login.j2", form=form) @app.route("/control", methods=["GET"]) def get_controls(): diff --git a/app/templates/index.j2 b/app/templates/index.j2 index ce0d531..656b187 100644 --- a/app/templates/index.j2 +++ b/app/templates/index.j2 @@ -7,6 +7,15 @@ {{ navbar(url_for) }}
+ {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %} {{ footer() }} diff --git a/app/templates/login.j2 b/app/templates/login.j2 new file mode 100644 index 0000000..fe898cb --- /dev/null +++ b/app/templates/login.j2 @@ -0,0 +1,25 @@ +{% from "macros.j2" import header, footer, navbar %} + + + {{ header(url_for) }} + +
+ {{ navbar(url_for) }} +

Sign In

+
+ {{ form.hidden_tag() }} +

+ {{ form.username.label }}
+ {{ form.username(size=32) }} +

+

+ {{ form.password.label }}
+ {{ form.password(size=32) }} +

+

{{ form.remember_me() }} {{ form.remember_me.label }}

+

{{ form.submit() }}

+
+ {{ footer() }} +
+ + -- cgit v1.3.1 From d5d64f2b1047f864811d23f19f109a6513c22a2a Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Sun, 13 Jul 2025 18:20:04 +0200 Subject: Working version with several bugs --- app/controller.py | 33 ++++++++++++----------- app/model.py | 68 +++++++++++++++++++++++++++++++---------------- app/routes.py | 7 ++--- app/templates/controls.j2 | 4 +-- app/templates/games.j2 | 12 ++++----- app/templates/macros.j2 | 2 +- app/utilities.py | 4 +-- 7 files changed, 78 insertions(+), 52 deletions(-) (limited to 'app/templates') diff --git a/app/controller.py b/app/controller.py index 1937af8..5bdeba7 100644 --- a/app/controller.py +++ b/app/controller.py @@ -2,7 +2,7 @@ from math import floor from app import app, db from datetime import date from operator import attrgetter -from sqlalchemy import select, func +from sqlalchemy import select, func, Date from app.model import ( Player, Players, @@ -28,19 +28,19 @@ class Session: players: Players = Players() def __init__(self): - games = db.session.query(Game).filter(Game.date.date() == date.today()).all() - if len(games) > 0: - return - 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() + with app.app_context(): + 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() def round(self) -> int: - query = select(func.max(Game.round_id)).where(Game.date.date() == date.today()) - round_id = db.session.scalars(query).first() + games = db.session.query(Game).filter(func.date(Game.date) == date.today()).all() + round_id = max([game.round_id for game in games]) + print(round_id) if round_id is None: raise ValueError(f"Database isn't initialized") return round_id @@ -48,7 +48,7 @@ class Session: def all(self) -> list[DisplayGame]: games = ( db.session.query(Game) - .filter(Game.date.date() == date.today()) + .filter(func.date(Game.date) == date.today()) .filter(Game.round_id == self.round()) .order_by(Game.court_id) .all() @@ -60,6 +60,7 @@ class Session: 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) @@ -71,7 +72,9 @@ class Session: reserved_games = get_placeholder_games(reserved_courts, "RESERVED") active_courts = self.courts.active() active_players = self.players.active() - game_count = floor(len(active_players) / app.config["PLAYER_PER_COURT"]) + for player in active_players: + print(player) + game_count = floor(len(active_players) / app.config["PLAYERS_PER_COURT"]) game_count = min(len(active_courts), game_count) used_courts = active_courts[:game_count] unused_courts = active_courts[game_count:] @@ -97,7 +100,7 @@ class Session: return self.all() def reset(self) -> list[DisplayGame]: - db.session.query(Game).filter(Game.date.date() == date.today()).delete() + db.session.query(Game).filter(Game.date.cast(Date) == date.today()).delete() db.session.commit() self.__init__() return self.all() diff --git a/app/model.py b/app/model.py index 2833bb1..56921f4 100644 --- a/app/model.py +++ b/app/model.py @@ -31,11 +31,12 @@ class Court(db.Model): class Courts: def __init__(self): - if app.config["COURT_COUNT"] is not None: - db.session.query(Court).delete() - for i in range(app.config["COURT_COUNT"]): - db.session.add(Court()) - db.session.commit() + 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) @@ -69,9 +70,17 @@ 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="---") - team: Mapped[int] = mc(Integer, index=True, default=0) - status: Mapped[bool] = mc(Boolean, default=False) - level: Mapped[int] = mc(Integer, default=app.config["MIN_PLAYER_LEVEL"]) + team: Mapped[int] = mc(Integer, index=True) + status: Mapped[bool] = mc(Boolean) + level: Mapped[int] = mc(Integer) + + def __init__(self, id, first_name, last_name, team = 0, status = False, level = int(app.config["MIN_PLAYER_LEVEL"])): + self.id = id + self.first_name = first_name + self.last_name = last_name + self.team = team + self.status = status + self.level = level def __repr__(self): return f"{self.first_name} {self.last_name} is {"active" if self.status else "inactive"}" @@ -80,8 +89,8 @@ class Player(db.Model): def initialize_random_database( player_count: int = 80, guest_per_level: int = 4 ) -> None: - db.session.add(Player(*{"first_name": "---", "last_name": ""})) - db.session.add(Player(*{"first_name": "RESERVED", "last_name": ""})) + 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): @@ -98,29 +107,32 @@ def initialize_random_database( app.config["MIN_PLAYER_LEVEL"], app.config["MAX_PLAYER_LEVEL"] ), } - db.session.add(Player(*args)) + 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.add(Player(**args)) db.session.commit() class Players: def __init__(self) -> None: - if app.config["DATABASE_CSV"] is not None: - data = read_csv(app.config["DATABASE_CSV"]) - db.session.query(Player).delete() - for entry in data.to_dict("records"): - args = entry | {"status": False} - db.session.add(Player(*args)) - db.session.commit() - else: - if len(self.all()) == 0: - initialize_random_database() + 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)) + db.session.commit() + else: + if len(self.all()) == 0: + initialize_random_database() def default_id(self): query = select(Player).where(Player.first_name == "---") @@ -184,12 +196,22 @@ class Game(db.Model): player_4: Mapped[int] = mc(ForeignKey(Player.id), index=True) date: Mapped[datetime] = mc(index=True, default=lambda: date.today()) + def __init__(self, round_id: int, court_id: int, player_1: int, player_2: int, player_3: int, player_4: int): + self.round_id = round_id + self.court_id = court_id + self.player_1 = player_1 + self.player_2 = player_2 + self.player_3 = player_3 + self.player_4 = player_4 + + class DisplayPlayer: def __init__(self, player: Player): self.id = player.id - self.name = f"{player.first_name} {player.last_name}" + self.first_name = player.first_name + self.last_name = player.last_name @dataclass diff --git a/app/routes.py b/app/routes.py index 2cc6009..bc85e48 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,7 +1,7 @@ from app import app from app.forms import LoginForm from app.controller import Session -from flask import render_template, request, url_for, Response, flash, redirect +from flask import render_template, request, url_for, Response, flash, redirect, get_template_attribute, render_template_string g = Session() @@ -50,8 +50,9 @@ def reset_games(): def toggle_player(player_request): id = int(player_request) player = g.players.toggle(id) - t = '{% from "macros.j2" import player_button %}{{player_button(player)}}' - return render_template(t, player=player) + button = get_template_attribute("macros.j2", "player_button") + t = '{% from "macros.j2" import player_button %}' + return render_template_string("{{player_button(player)}}", player=player, player_button=button) @app.route("/players", methods=["GET"]) diff --git a/app/templates/controls.j2 b/app/templates/controls.j2 index 0183c97..eba086d 100644 --- a/app/templates/controls.j2 +++ b/app/templates/controls.j2 @@ -1,8 +1,8 @@ {% from "macros.j2" import court_button %}
- {% for key, value in courts.items() %} - {{ court_button(key, value) }} + {% for court in courts %} + {{ court_button(court.id, court.status) }} {% endfor %}
diff --git a/app/templates/games.j2 b/app/templates/games.j2 index 46150f7..ccfd395 100644 --- a/app/templates/games.j2 +++ b/app/templates/games.j2 @@ -9,12 +9,12 @@ {% for game in games %} - - {{ game.court }} - {{ game.team1.player1.first }}{{ " " + game.team1.player1.last }} - {{ game.team1.player2.first }}{{ " " + game.team1.player2.last }} - {{ game.team2.player1.first }}{{ " " + game.team2.player1.last }} - {{ game.team2.player2.first }}{{ " " + game.team2.player2.last }} + + {{ 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 }} {% endfor %} diff --git a/app/templates/macros.j2 b/app/templates/macros.j2 index 2974744..a4b2fc1 100644 --- a/app/templates/macros.j2 +++ b/app/templates/macros.j2 @@ -7,7 +7,7 @@ {% macro player_button(player) -%} + hx-swap="outerHTML"> {{ player.first_name + " " + player.last_name}} {%- endmacro %} {% macro guest_control(level, value) -%} diff --git a/app/utilities.py b/app/utilities.py index 00dc14a..c84e1bf 100644 --- a/app/utilities.py +++ b/app/utilities.py @@ -89,7 +89,7 @@ def get_team_games( ) -> list[DisplayGame]: games = [] for court, (_, players) in zip(courts, teams.items()): - players = sample(players, app.config["PLAYER_PER_COURT"]) + 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( @@ -101,7 +101,7 @@ def get_team_games( def get_shuffle_games( courts: list[Court], players: Sequence[Player], total_levels: int ) -> list[DisplayGame]: - required_player_count = len(courts) * app.config["PLAYER_PER_COURT"] + required_player_count = len(courts) * app.config["PLAYERS_PER_COURT"] selected_players = select_players(players, required_player_count) court_players = group_by_four(selected_players, total_levels) games = [] -- cgit v1.3.1 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(-) (limited to 'app/templates') 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