diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/__init__.py | 141 | ||||
| -rw-r--r-- | app/config.py | 15 | ||||
| -rw-r--r-- | app/controller.py | 194 | ||||
| -rw-r--r-- | app/model.py | 110 | ||||
| -rw-r--r-- | app/static/custom.css | 34 | ||||
| -rw-r--r-- | app/static/favicon.ico | bin | 15406 -> 0 bytes | |||
| -rw-r--r-- | app/static/file.js | 11 | ||||
| -rw-r--r-- | app/static/timer.js | 91 | ||||
| -rw-r--r-- | app/templates/controls.j2 | 48 | ||||
| -rw-r--r-- | app/templates/games.j2 | 21 | ||||
| -rw-r--r-- | app/templates/index.j2 | 16 | ||||
| -rw-r--r-- | app/templates/login.html | 23 | ||||
| -rw-r--r-- | app/templates/macros.j2 | 45 | ||||
| -rw-r--r-- | app/templates/players.j2 | 40 | ||||
| -rw-r--r-- | app/utilities.py | 172 |
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 Binary files differdeleted file mode 100644 index 64eab0a..0000000 --- a/app/static/favicon.ico +++ /dev/null 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() |
