aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorKaran Jayachandra <me@karanj.com>2025-11-14 18:10:49 +0100
committerKaran Jayachandra <me@karanj.com>2025-11-14 18:10:49 +0100
commit202cf761e5162720f25521a75ae7cd6a4e0e5456 (patch)
tree0e57324bc72629cba3ed817b3439780f6959e509 /app
parent90390365bd93a4ee086c2931bb70ffa571a726b9 (diff)
parent56eb065a8165b3f508a35734adbb00c76bee012a (diff)
Merge branch 'develop' into 'main'
Develop See merge request KaranJayachandra/match_up!21
Diffstat (limited to 'app')
-rw-r--r--app/__init__.py141
-rw-r--r--app/config.py15
-rw-r--r--app/controller.py194
-rw-r--r--app/model.py110
-rw-r--r--app/static/custom.css34
-rw-r--r--app/static/favicon.icobin15406 -> 0 bytes
-rw-r--r--app/static/file.js11
-rw-r--r--app/static/timer.js91
-rw-r--r--app/templates/controls.j248
-rw-r--r--app/templates/games.j221
-rw-r--r--app/templates/index.j216
-rw-r--r--app/templates/login.html23
-rw-r--r--app/templates/macros.j245
-rw-r--r--app/templates/players.j240
-rw-r--r--app/utilities.py172
15 files changed, 0 insertions, 961 deletions
diff --git a/app/__init__.py b/app/__init__.py
deleted file mode 100644
index 0e0ea09..0000000
--- a/app/__init__.py
+++ /dev/null
@@ -1,141 +0,0 @@
-from pandas import read_csv
-from functools import wraps
-from app.config import Config
-from flask_migrate import Migrate
-from flask_sqlalchemy import SQLAlchemy
-from flask_simplelogin import SimpleLogin
-from werkzeug.utils import secure_filename
-from flask_simplelogin import is_logged_in
-from flask import (
- Flask,
- render_template_string,
- render_template,
- request,
- redirect,
- url_for,
- Response,
- get_template_attribute,
-)
-
-app = Flask(__name__)
-app.config.from_object(Config)
-db = SQLAlchemy(app)
-migrate = Migrate(app, db)
-SimpleLogin(app)
-
-
-from app.controller import Session # noqa: E402
-
-g = Session()
-
-
-@app.route("/", methods=["GET"])
-def home():
- return render_template("index.j2", url_for=url_for, login_status=is_logged_in())
-
-
-@app.route("/control", methods=["GET"])
-def get_controls():
- return render_template("controls.j2", courts=g.courts.all())
-
-
-@app.route("/players", methods=["GET"])
-def get_list_of_players():
- return render_template(
- "players.j2",
- regulars=g.players.regulars(),
- guests=g.players.guests(),
- url_for=url_for,
- login_status=is_logged_in(),
- )
-
-
-@app.route("/courts", methods=["GET"])
-def get_list_of_courts():
- return render_template("courts.j2", courts=g.courts.all())
-
-
-def login_required(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if not is_logged_in():
- response = Response("User isn't logged in!")
- response.headers["HX-Redirect"] = url_for("simplelogin.login")
- return response
- return f(*args, **kwargs)
-
- return decorated_function
-
-
-@app.route("/propose", methods=["POST"])
-@login_required
-def get_random_games():
- g.levels = int(request.form["levels"].split()[0])
- g.teams = request.form["team"] == "Teams"
- return render_template("games.j2", games=g.propose(), title="Proposal")
-
-
-@app.route("/confirm", methods=["POST"])
-@login_required
-def confirm_games():
- return render_template("games.j2", games=g.confirm(), title=f"Round: {g.round()}")
-
-
-@app.route("/clear", methods=["POST"])
-def clear_games():
- return render_template(
- "games.j2", games=g.current_round(), title=f"Round: {g.round()}"
- )
-
-
-@app.route("/reset", methods=["POST"])
-@login_required
-def reset_games():
- return render_template("games.j2", games=g.reset(), title=f"Round: {g.round()}")
-
-
-@app.route("/player-toggle/<player_request>", methods=["POST"])
-@login_required
-def toggle_player(player_request):
- id = int(player_request)
- player = g.players.toggle(id)
- button = get_template_attribute("macros.j2", "player_button")
- return render_template_string("{{button(player)}}", player=player, button=button)
-
-
-@app.route("/reset_players", methods=["POST"])
-@login_required
-def reset_all_players():
- g.players.reset()
- return redirect(url_for("players"))
-
-
-@app.route("/update_players", methods=["POST"])
-@login_required
-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("All columns aren't available in the data.")
- except: # noqa: E722
- return response
- g.players.update(data)
- return redirect(url_for("players"))
-
-
-@app.route("/court-toggle/<court_number>", methods=["POST"])
-@login_required
-def toggle_court(court_number):
- id = int(court_number)
- court = g.courts.toggle(id)
- button = get_template_attribute("macros.j2", "court_button")
- return render_template_string("{{button(court)}}", court=court, button=button)
diff --git a/app/config.py b/app/config.py
deleted file mode 100644
index 5bc65b3..0000000
--- a/app/config.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from os import environ
-
-
-class Config:
- SQLALCHEMY_DATABASE_URI = environ.get("SQLALCHEMY_DATABASE_URI")
- SECRET_KEY = environ.get("SECRET_KEY")
- GUEST_COUNT = 4
- COURT_COUNT = 12
- MIN_PLAYER_LEVEL = 1
- MAX_PLAYER_LEVEL = 10
- PLAYERS_PER_COURT = 4
- # Default players as dictionary of first names and primary keys
- DEFAULTS = {"---": 1, "RESERVED": 2}
- # Guest players as dictionary of first names and play level
- GUESTS = {"Beginner": 1, "Novice": 3, "Intermediate": 5}
diff --git a/app/controller.py b/app/controller.py
deleted file mode 100644
index d5bdbcb..0000000
--- a/app/controller.py
+++ /dev/null
@@ -1,194 +0,0 @@
-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, select, not_
-from app.model import (
- Player,
- Court,
- Game,
- DisplayGame,
-)
-from app.utilities import (
- get_placeholder_games,
- 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():
- db.session.query(Court).delete()
- for _ in range(app.config["COURT_COUNT"]):
- db.session.add(Court())
- db.session.commit()
-
- def get(self, id: int) -> Court:
- court = db.session.get(Court, id)
- if court is None:
- raise ValueError(f"Court not found: {id}")
- return court
-
- def toggle(self, id: int) -> Court:
- court = self.get(id)
- court.status = not court.status
- db.session.commit()
- return court
-
- 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) -> Player:
- player = db.session.get(Player, id)
- if player is None:
- raise ValueError(f"Player not found: {id}")
- return player
-
- def toggle(self, id: int) -> Player:
- player = self.get(id)
- 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
- proposal: list[DisplayGame] = []
-
- 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():
- db.create_all()
- self.courts = Courts()
- self.players = Players()
- if len(self._today()) == 0:
- query = select(Player).where(Player.first_name == "---")
- player = db.session.scalars(query).first()
- if player is None:
- raise ValueError("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 = self._today()
- round_id = max([game.round_id for game in games])
- if round_id is None:
- raise ValueError("Database isn't initialized")
- return round_id
-
- def current_round(self) -> list[DisplayGame]:
- games = (
- db.session.query(Game)
- .filter(func.date(Game.date) == date.today())
- .filter(Game.round_id == self.round())
- .order_by(Game.court_id)
- .all()
- )
- games = [DisplayGame(game) for game in games]
- return games
-
- def propose(self) -> list[DisplayGame]:
- reserved_courts = self.courts.inactive()
- 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["PLAYERS_PER_COURT"])
- game_count = min(len(active_courts), game_count)
- used_courts = active_courts[:game_count]
- unused_courts = active_courts[game_count:]
- blank_games = get_placeholder_games(unused_courts, "---")
- active_teams = {}
- if self.teams:
- active_teams, active_players = separate_teams_and_players(active_players)
- team_courts = used_courts[: len(active_teams)]
- shuffle_courts = used_courts[len(active_teams) :]
- 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"))
- return self.proposal
-
- def confirm(self) -> list[DisplayGame]:
- if len(self.proposal) == 0:
- return self.current_round()
- round_id = self.round() + 1
- for game in self.proposal:
- db.session.add(game.to_game(round_id))
- db.session.commit()
- self.proposal = []
- return self.current_round()
-
- def reset(self) -> list[DisplayGame]:
- db.session.query(Game).filter(func.date(Game.date) == date.today()).delete()
- db.session.commit()
- self.__init__()
- return self.current_round()
diff --git a/app/model.py b/app/model.py
deleted file mode 100644
index 22424d4..0000000
--- a/app/model.py
+++ /dev/null
@@ -1,110 +0,0 @@
-from app import app, db
-from sqlalchemy.orm import Mapped
-from datetime import date, datetime
-from typing import Optional, Annotated
-from sqlalchemy.orm import mapped_column as mc
-from sqlalchemy import String, Boolean, Integer, ForeignKey
-
-
-class User(db.Model):
- id: Mapped[int] = mc(primary_key=True)
- username: Mapped[str] = mc(String(64), index=True, unique=True)
- password_hash: Mapped[Optional[str]] = mc(String(256))
-
- def __repr__(self):
- return f"User {self.username}"
-
-
-class Court(db.Model):
- id: Mapped[int] = mc(primary_key=True)
- status: Mapped[bool] = mc(Boolean, default=True)
-
- def __repr__(self):
- return f"Court {self.id} is {'active' if self.status else 'inactive'}"
-
-
-class Player(db.Model):
- id: Mapped[int] = mc(primary_key=True)
- 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)
-
- 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'}"
-
-
-class Game(db.Model):
- id: Mapped[int] = mc(primary_key=True)
- round_id: Mapped[int] = mc(Integer, index=True)
- court_id: Mapped[int] = mc(ForeignKey(Court.id), index=True)
- player_1: Mapped[int] = mc(ForeignKey(Player.id), index=True)
- player_2: Mapped[int] = mc(ForeignKey(Player.id), index=True)
- player_3: Mapped[int] = mc(ForeignKey(Player.id), index=True)
- 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: Optional[int],
- court_id: int,
- player_1: int,
- player_2: int,
- player_3: int,
- player_4: int,
- ):
- 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
- self.player_3 = player_3
- self.player_4 = player_4
-
-
-class DisplayGame:
- 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("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/static/custom.css b/app/static/custom.css
deleted file mode 100644
index a5ca36f..0000000
--- a/app/static/custom.css
+++ /dev/null
@@ -1,34 +0,0 @@
-button, input, select {
- width: 100%;
- height: 100%;
-}
-td {
- width: 20%;
-}
-.grid {
- margin-top: 20px;
- margin-bottom: 20px;
-}
-footer {
- text-align: center;
-}
-.parent, .timer {
- display: grid;
- grid-column-gap: 20px;
- grid-row-gap: 20px;
- margin-top: 20px;
- margin-bottom: 20px;
- grid-auto-columns: 1fr;
- grid-auto-rows: 1fr;
-}
-@media (min-width: 900px) {
- table {
- font-size: xx-large;
- }
- .parent {
- grid-template-columns: repeat(4, 1fr);
- }
- .timer {
- grid-template-columns: 4fr repeat(2, 1fr);
- }
-}
diff --git a/app/static/favicon.ico b/app/static/favicon.ico
deleted file mode 100644
index 64eab0a..0000000
--- a/app/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/app/static/file.js b/app/static/file.js
deleted file mode 100644
index 403ddf6..0000000
--- a/app/static/file.js
+++ /dev/null
@@ -1,11 +0,0 @@
-window.onload = function() {
- const fileInput = document.querySelector("#file-js input[type=file]");
- if (fileInput !== null) {
- 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/static/timer.js b/app/static/timer.js
deleted file mode 100644
index e8e7854..0000000
--- a/app/static/timer.js
+++ /dev/null
@@ -1,91 +0,0 @@
-let minsPerRound = Number(sessionStorage.getItem("minsPerRound")) || 12;
-let timerRunningFlag = Number(sessionStorage.getItem("timerRunningFlag")) || 0;
-let deadline = Number(sessionStorage.getItem("deadline")) || new Date().getTime();
-let clock = 0;
-
-function setTimer(m, s) {
- let timer = document.getElementById("timer-display");
- m = m < 10 ? ("0" + m) : m;
- s = s < 10 ? ("0" + s) : s;
- timer.value = `${m}:${s}`;
-}
-
-function setSessionData() {
- sessionStorage.setItem("deadline", deadline);
- sessionStorage.setItem("minsPerRound", minsPerRound);
- sessionStorage.setItem("timerRunningFlag", timerRunningFlag);
-}
-
-function resetTimer() {
- clearInterval(clock);
- setTimer(minsPerRound, 0);
- timerRunningFlag = 0;
- setSessionData();
-}
-
-function updateTimer(milliseconds) {
- const m = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
- const s = Math.floor((milliseconds % (1000 * 60)) / 1000);
- setTimer(m, s);
-}
-
-function refreshTimer() {
- const now = new Date().getTime();
- const millisecondsToDeadline = deadline - now;
- if (millisecondsToDeadline > 0) {
- updateTimer(millisecondsToDeadline);
- return;
- }
- resetTimer();
- const notification = new Notyf({
- duration: 30 * 1000,
- position: {x: 'center', y: 'top'},
- types: [{
- type: 'success',
- background: 'indianred',
- duration: 1000 * 1000,
- dismissible: true
- }]
- });
- notification.success("Round completed");
-}
-
-function startTimer(fresh) {
- if (fresh) {
- now = new Date().getTime();
- const millisecondsToDeadline = minsPerRound * 60 * 1000;
- deadline = now + millisecondsToDeadline;
- }
- timerRunningFlag = 1;
- setSessionData();
- clock = setInterval(refreshTimer, 1000);
-}
-
-window.onload = function () {
- if (timerRunningFlag)
- startTimer(0);
- else resetTimer();
-
- document.getElementById("timer-start").addEventListener('click', ()=> {
- if (timerRunningFlag != 0) return;
- startTimer(1);
- });
-
- document.getElementById("timer-stop").addEventListener('click', ()=>{
- resetTimer();
- });
-
- document.getElementById("timer-inc").addEventListener('click', ()=> {
- if (timerRunningFlag == 0) {
- minsPerRound += 1;
- resetTimer();
- }
- });
-
- document.getElementById("timer-dec").addEventListener('click', ()=> {
- if (timerRunningFlag == 0 && minsPerRound != 0) {
- minsPerRound -= 1;
- resetTimer();
- }
- });
-} \ No newline at end of file
diff --git a/app/templates/controls.j2 b/app/templates/controls.j2
deleted file mode 100644
index 97351bf..0000000
--- a/app/templates/controls.j2
+++ /dev/null
@@ -1,48 +0,0 @@
-{% from "macros.j2" import court_button %}
-<h3>Timer</h3>
-<div class="timer">
- <div role="group" style="height: 100%">
- <button id="timer-dec"><i class="fas fa-light fa-minus"></i></button>
- <input id="timer-display" type="text" style="text-align:center; height: 100%" readonly></input>
- <button id="timer-inc"><i class="fas fa-light fa-plus"></i></button>
- </div>
- <button id="timer-start"><i class="fas fa-light fa-play"></i></button>
- <button id="timer-stop"><i class="fas fa-light fa-stop"></i></button>
-</div>
-<h3>Generation</h3>
-<div class="grid">
- <button class="button is-dark is-responsive"
- hx-post="/propose"
- hx-target="#games"
- hx-include="[name='levels'], [name='team']">Generate</button>
- <button class="button is-dark is-responsive"
- hx-post="/confirm"
- hx-target="#games">Confirm</button>
- <button class="button is-dark is-responsive"
- hx-post="/reset"
- hx-target="#games">RESET!</button>
-</div>
-<div class="grid">
- <div class="select is-responsive">
- <select name="levels">
- <option>10 Levels</option>
- <option>8 Levels</option>
- <option>6 Levels</option>
- <option>4 Levels</option>
- <option>2 Levels</option>
- <option>0 Levels</option>
- </select>
- </div>
- <div class="select is-responsive">
- <select name="team">
- <option>Random</option>
- <option>Teams</option>
- </select>
- </div>
-</div>
-<h3>Courts</h3>
-<div class="parent">
-{% for court in courts %}
- {{ court_button(court) }}
-{% endfor %}
-</div> \ No newline at end of file
diff --git a/app/templates/games.j2 b/app/templates/games.j2
deleted file mode 100644
index 00ffb71..0000000
--- a/app/templates/games.j2
+++ /dev/null
@@ -1,21 +0,0 @@
-<div id="games" class="overflow-auto">
- <h3> {{ title }} </h3>
- <table>
- <thead>
- <th align="center"> Court </th>
- <th colspan="2" align="center"> Team 1 </th>
- <th colspan="2" align="center"> Team 2 </th>
- </thead>
- <tbody>
- {% for game in games %}
- <tr {% if game.team_1[0].first_name == 'RESERVED' %} data-theme="dark" {% endif %}>
- <td align="center" style="width: 8%;">{{ game.court.id }}</th>
- <td style="width: 23%;"><strong>{{ game.team_1[0].first_name }}</strong>{{ " " + game.team_1[0].last_name }}</th>
- <td style="width: 23%;"><strong>{{ game.team_1[1].first_name }}</strong>{{ " " + game.team_1[1].last_name }}</th>
- <td style="width: 23%;"><strong>{{ game.team_2[0].first_name }}</strong>{{ " " + game.team_2[0].last_name }}</th>
- <td style="width: 23%;"><strong>{{ game.team_2[1].first_name }}</strong>{{ " " + game.team_2[1].last_name }}</th>
- </tr>
- {% endfor %}
- </tbody>
- </table>
-<div> \ No newline at end of file
diff --git a/app/templates/index.j2 b/app/templates/index.j2
deleted file mode 100644
index af6ca4c..0000000
--- a/app/templates/index.j2
+++ /dev/null
@@ -1,16 +0,0 @@
-{% from "macros.j2" import header, footer, navbar %}
-<!doctype html>
-<html lang="en">
- {{ header(url_for) }}
- <body>
- <header class="container">{{ navbar(url_for, login_status) }}</header>
- <main class="container">
- <h2>Games</h2>
- <div hx-post="/clear" hx-swap="outerHTML" hx-trigger="load"></div>
- <h2>Controls</h2>
- <div hx-get="/control" hx-swap="outerHTML" hx-trigger="load"></div>
- </main>
- <footer class="container">{{ footer() }}</footer>
- <script src="{{ url_for('static', filename='timer.js') }}"></script>
- </body>
-</html>
diff --git a/app/templates/login.html b/app/templates/login.html
deleted file mode 100644
index e3109b6..0000000
--- a/app/templates/login.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{% from "macros.j2" import header, footer, navbar %}
-<!doctype html>
-<html lang="en">
-{{ header(url_for) }}
-<body>
- <header class="container">{{ navbar(url_for) }}</header>
- <main class="container">
- <form action="{{ url_for('simplelogin.login') }}" method="post">
- <div class="form-group">
- {{ form.csrf_token }}
- {{form.username.label(class_="label")}}<div class="control form-control">{{ form.username(class_="input") }}
- </div><br>
- {{form.password.label(class_="label")}}<div class="control form-control"> {{ form.password(class_="input") }}
- </div><br>
- {% if next %}<input type="hidden" name="next" value="{{next}}">{% endif %}
- </form>
- <input class="button is-dark" type="submit" value="Login">
- </form>
- </main>
- <footer class="container">{{ footer() }}</footer>
-</body>
-
-</html> \ No newline at end of file
diff --git a/app/templates/macros.j2 b/app/templates/macros.j2
deleted file mode 100644
index e82b910..0000000
--- a/app/templates/macros.j2
+++ /dev/null
@@ -1,45 +0,0 @@
-{% macro court_button(court) -%}
-<button class= "{% if court.status %}{% else %}contrast{% endif %}"
- hx-post="{{ "/court-toggle/" ~ court.id}}"
- hx-swap="outerHTML"> Court {{ court.id }}</button>
-{%- endmacro %}
-
-{% macro player_button(player) -%}
-<button class="{% if player.status %}{% else %}contrast{% endif %}"
- hx-post="{{ "/player-toggle/" ~ player.id}}"
- hx-swap="outerHTML"> {{ player.first_name + " " + player.last_name}} </button>
-{%- endmacro %}
-
-{% macro header(url_for) -%}
-<head>
-<meta charset="UTF-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Match Up!</title>
-<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
-<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
-<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/notyf@3/notyf.min.css">
-<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
-<link rel="stylesheet" href="{{ url_for('static', filename='custom.css') }}">
-<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js"></script>
-<script src="https://cdn.jsdelivr.net/npm/notyf@3/notyf.min.js"></script>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/js/fontawesome.min.js"></script>
-</head>
-{%- endmacro %}
-
-{% macro footer() -%}
-<div class="container">Made by <a href="https://karanjayachandra.com/">Karan</a> with <i class="fas fa-heart"></i></div>
-{%- endmacro %}
-
-
-{% macro navbar(url_for, login_status) -%}
-<nav>
- <ul>
- <li><strong>Match Up!</strong></li>
- </ul>
- <ul>
- <li><a href="/">Games</a></li>
- <li><a href="/players">Players</a></li>
- {% if login_status %}<li><a href="/logout/">Logout</a></li>{% else %}<li><a href="/login/">Login</a></li>{% endif %}
- </ul>
-</nav>
-{%- endmacro %} \ No newline at end of file
diff --git a/app/templates/players.j2 b/app/templates/players.j2
deleted file mode 100644
index bacb215..0000000
--- a/app/templates/players.j2
+++ /dev/null
@@ -1,40 +0,0 @@
-{% from "macros.j2" import header, navbar, footer, player_button, guest_control %}
-<!doctype html>
-<html lang="en">
- {{ header(url_for) }}
- <body>
- <header class="container">{{ navbar(url_for, login_status) }}</header>
- <main class="container">
- <h3 >Members</h3>
- <div class="parent">
- {% for regular in regulars %}
- <div class="cell"> {{ player_button(regular) }} </div>
- {% endfor %}
- </div>
- <h3>Guests</h3>
- <div class="parent">
- {% for guest in guests %}
- <div class="cell"> {{ player_button(guest) }} </div>
- {% endfor %}
- </div>
- <h3>Controls</h3>
- <div class="grid">
- <button class="button is-dark is-responsive"
- hx-post="/reset_players">RESET!</button>
- </div>
- <h3>Player Update</h3>
- <div class="grid">
- <form method="POST" enctype="multipart/form-data" hx-post="/update_players">
- <div id="file-js" class="file has-name is-fullwidth">
- <label class="file-label">
- <input class="file-input" type="file" name="players" />
- </label>
- <button class="button is-dark is-responsive">UPDATE!</button>
- </div>
- </form>
- </div>
- <footer class="container">{{ footer() }}</footer>
- <script src="{{ url_for('static', filename='file.js') }}"></script>
- </main>
- </body>
-</html> \ No newline at end of file
diff --git a/app/utilities.py b/app/utilities.py
deleted file mode 100644
index d8bd3da..0000000
--- a/app/utilities.py
+++ /dev/null
@@ -1,172 +0,0 @@
-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 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]:
- indices = list(range(len(a)))
- shuffle(indices)
- a = [a[index] for index in indices]
- b = [b[index] for index in indices]
- return a, b
-
-
-def _pick_from_list_after_sorting_other(a: list, b: list[int]) -> list:
- indices = [i[0] for i in sorted(enumerate(b), key=lambda x: x[1])]
- return [a[index] for index in indices]
-
-
-def sample_list(indices: list, cost: list[int], count=None) -> list:
- if count is None:
- count = len(indices)
- indices, cost = _shuffle_two_lists(indices, cost)
- indices = _pick_from_list_after_sorting_other(indices, cost)[:count]
- return indices
-
-
-def _normalize_skill(level: int, new_max: int) -> int:
- scaling = (new_max - 1) / int(app.config["MAX_PLAYER_LEVEL"])
- new_level = round(scaling * level) + 1
- return new_level
-
-
-def select_players(players: Sequence[Player], count: int) -> list[Player]:
- games = db.session.query(Game).filter(func.date(Game.date) == date.today()).all()
- player_identifiers = []
- for game in games:
- for i in range(4):
- player_identifiers.append(getattr(game, f"player_{i + 1}"))
- identifiers = [player.id for player in players]
- play_count = [player_identifiers.count(player.id) for player in players]
- ids = sample_list(identifiers, play_count, count)
- selected_players = [player for player in players if player.id in ids]
- return selected_players
-
-
-def group_by_four(players: list[Player], new_max: int) -> list[list[Player]]:
- identifiers = [player.id for player in players]
- player_levels = [_normalize_skill(int(player.level), new_max) for player in players]
- identifiers = sample_list(identifiers, player_levels)
- court_players = []
- for i in range(0, len(players), app.config["PLAYERS_PER_COURT"]):
- court_ids = identifiers[i : i + app.config["PLAYERS_PER_COURT"]]
- selected_players = [player for player in players if player.id in court_ids]
- court_players.append(selected_players)
- return court_players
-
-
-def separate_teams_and_players(
- players: Sequence[Player],
-) -> tuple[dict[int, list[Player]], list[Player]]:
- unique_team_id = {player.team for player in players}
- grouped_players_by_team = {team_id: [] for team_id in unique_team_id}
- for player in players:
- team = player.team
- grouped_players_by_team[team].append(player)
- active_teams = {}
- players_without_team = []
- for team_id in grouped_players_by_team:
- if team_id == 0:
- players_without_team += [
- player for player in grouped_players_by_team[team_id]
- ]
- else:
- if len(grouped_players_by_team[team_id]) >= 4:
- active_teams[team_id] = grouped_players_by_team[team_id]
- else:
- players_without_team += [
- player for player in grouped_players_by_team[team_id]
- ]
- return active_teams, players_without_team
-
-
-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("Database isn't initialized.")
- 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))) # type: ignore
- for court in courts
- ]
- return games
-
-
-def get_team_games(
- courts: list[Court], teams: dict[int, list[Player]]
-) -> list[DisplayGame]:
- 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)
- game = Game(
- None, court.id, players[0].id, players[3].id, players[1].id, players[2].id
- )
- games.append(DisplayGame(game))
- return games
-
-
-def get_shuffle_games(
- courts: list[Court], players: Sequence[Player], total_levels: int
-) -> list[DisplayGame]:
- 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 = []
- for court, players in zip(courts, court_players):
- 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()