From 4aee68096c11ce1e3762fb8cffbde2a14dfa3cd0 Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Mon, 14 Jul 2025 19:51:45 +0200 Subject: Added the login for changes --- app/__init__.py | 132 +++++++++++++++++++++++++++++++++++++++++++++- app/config.py | 7 +-- app/controller.py | 36 ++++++------- app/model.py | 8 +-- app/routes.py | 111 -------------------------------------- app/static/custom.js | 10 ++-- app/templates/controls.j2 | 2 +- app/templates/index.j2 | 9 ---- app/templates/login.html | 23 ++++++++ app/templates/login.j2 | 25 --------- app/templates/macros.j2 | 30 +++++------ app/utilities.py | 9 ++-- 12 files changed, 206 insertions(+), 196 deletions(-) delete mode 100644 app/routes.py create mode 100644 app/templates/login.html delete mode 100644 app/templates/login.j2 (limited to 'app') diff --git a/app/__init__.py b/app/__init__.py index 2e0a8be..3d3b498 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,11 +1,139 @@ -from flask import Flask +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 import routes + +from app.controller import Session # noqa: E402 + +g = Session() + + +@app.route("/", methods=["GET"]) +def home(): + return render_template("index.j2", url_for=url_for) + + +@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, + ) + + +@app.route("/courts", methods=["GET"]) +def get_list_of_courts(): + return render_template("courts.j2", courts=g.courts.all()) + + +@app.route("/propose", methods=["POST"]) +def get_random_games(): + if not is_logged_in(): + return redirect(url_for("login")) + 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"]) +def confirm_games(): + if not is_logged_in(): + return redirect(url_for("simplelogin.login")) + 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()}" + ) + +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("/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/", 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/", 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 index 761165f..5bc65b3 100644 --- a/app/config.py +++ b/app/config.py @@ -3,12 +3,13 @@ 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 - PLAYERS_PER_COURT = 4 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_COUNT = 4 # Guest players as dictionary of first names and play level - GUESTS = {"Beginner": 1, "Novice": 3, "Intermediate": 5} \ No newline at end of file + GUESTS = {"Beginner": 1, "Novice": 3, "Intermediate": 5} diff --git a/app/controller.py b/app/controller.py index 1c2c5bf..3bfce38 100644 --- a/app/controller.py +++ b/app/controller.py @@ -22,7 +22,6 @@ from app.utilities import ( class Courts: - def __init__(self): with app.app_context(): courts = db.session.query(Court).all() @@ -31,14 +30,17 @@ class Courts: db.session.add(Court()) db.session.commit() - def toggle(self, id: int) -> bool: + def get(self, id: int) -> Court: 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 + 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 new_status + return court def active(self): return db.session.query(Court).where(Court.status).all() @@ -51,7 +53,6 @@ class Courts: class Players: - def __init__(self) -> None: with app.app_context(): if len(self.regulars()) == 0: @@ -64,16 +65,14 @@ class Players: db.session.add(Player(**args)) # type: ignore db.session.commit() - def get(self, id: int) -> bool: + def get(self, id: int) -> Player: player = db.session.get(Player, id) if player is None: - raise ValueError(f"User {id} doesn't exist.") - return player.status + raise ValueError(f"Player not found: {id}") + return player 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 = self.get(id) player.status = not player.status db.session.commit() return player @@ -117,8 +116,6 @@ class Session: levels: int = 10 teams: bool = False proposal: list[DisplayGame] = [] - courts: Courts = Courts() - players: Players = Players() def _today(self): games = ( @@ -128,12 +125,15 @@ class Session: 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(f"Database isn't initialized.") - player_data = {f"player_{i+1}": player.id for i in range(4)} + 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)) @@ -143,7 +143,7 @@ class Session: 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") + raise ValueError("Database isn't initialized") return round_id def current_round(self) -> list[DisplayGame]: diff --git a/app/model.py b/app/model.py index 3ec663e..22424d4 100644 --- a/app/model.py +++ b/app/model.py @@ -20,7 +20,7 @@ class Court(db.Model): status: Mapped[bool] = mc(Boolean, default=True) def __repr__(self): - return f"Court {self.id} is {"active" if self.status else "inactive"}" + return f"Court {self.id} is {'active' if self.status else 'inactive'}" class Player(db.Model): @@ -48,7 +48,7 @@ class Player(db.Model): self.level = level def __repr__(self): - return f"{self.first_name} {self.last_name} is {"active" if self.status else "inactive"}" + return f"{self.first_name} {self.last_name} is {'active' if self.status else 'inactive'}" class Game(db.Model): @@ -87,11 +87,11 @@ class DisplayGame: def __init__(self, game: Game): court = db.session.get(Court, game.court_id) if court is None: - raise ValueError(f"Unknown court") + raise ValueError("Unknown court") self.court = court players = [] for i in range(4): - player_id = getattr(game, f"player_{i+1}") + 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.") diff --git a/app/routes.py b/app/routes.py deleted file mode 100644 index f4ec0f2..0000000 --- a/app/routes.py +++ /dev/null @@ -1,111 +0,0 @@ -from app import app -from pandas import read_csv -from app.controller import Session -from werkzeug.utils import secure_filename -from flask import ( - render_template, - request, - url_for, - Response, - flash, - redirect, - get_template_attribute, -) -from flask import render_template_string as render_string - -g = Session() - - -@app.route("/", methods=["GET"]) -def home(): - return render_template("index.j2", url_for=url_for) - - -@app.route("/control", methods=["GET"]) -def get_controls(): - return render_template("controls.j2", courts=g.courts.all()) - - -@app.route("/propose", methods=["POST"]) -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"]) -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"]) -def reset_games(): - return render_template("games.j2", games=g.reset(), title=f"Round: {g.round()}") - - -@app.route("/player-toggle/", methods=["POST"]) -def toggle_player(player_request): - id = int(player_request) - player = g.players.toggle(id) - button = get_template_attribute("macros.j2", "player_button") - return render_string("{{button(player)}}", player=player, button=button) - - -@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, - ) - - -@app.route("/reset_players", methods=["POST"]) -def reset_all_players(): - g.players.reset() - response = Response("Players reset!") - 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): - id = int(court_number) - status = g.courts.toggle(id) - button = get_template_attribute("macros.j2", "court_button") - return render_string("{{button(id, status)}}", id=id, status=status, button=button) - - -@app.route("/courts", methods=["GET"]) -def get_list_of_courts(): - return render_template("courts.j2", courts=g.courts.all()) diff --git a/app/static/custom.js b/app/static/custom.js index 47f64ca..1d36493 100644 --- a/app/static/custom.js +++ b/app/static/custom.js @@ -87,9 +87,11 @@ document.getElementById("timer-dec").addEventListener('click', ()=> { }); 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; + 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/templates/controls.j2 b/app/templates/controls.j2 index eba086d..0acf5a6 100644 --- a/app/templates/controls.j2 +++ b/app/templates/controls.j2 @@ -2,7 +2,7 @@
{% for court in courts %} - {{ court_button(court.id, court.status) }} + {{ court_button(court) }} {% endfor %}
diff --git a/app/templates/index.j2 b/app/templates/index.j2 index 656b187..ce0d531 100644 --- a/app/templates/index.j2 +++ b/app/templates/index.j2 @@ -7,15 +7,6 @@ {{ navbar(url_for) }}
- {% with messages = get_flashed_messages() %} - {% if messages %} -
    - {% for message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% endif %} - {% endwith %} {{ footer() }} diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..a42f2c5 --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,23 @@ +{% from "macros.j2" import header, footer, navbar %} + + +{{ header(url_for) }} + +
+ {{ navbar(url_for) }} +
+
+ {{ form.csrf_token }} + {{form.username.label(class_="label")}}
{{ form.username(class_="input") }} +

+ {{form.password.label(class_="label")}}
{{ form.password(class_="input") }} +

+ {% if next %}{% endif %} + + + + {{ footer() }} +
+ + + \ No newline at end of file diff --git a/app/templates/login.j2 b/app/templates/login.j2 deleted file mode 100644 index fe898cb..0000000 --- a/app/templates/login.j2 +++ /dev/null @@ -1,25 +0,0 @@ -{% 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() }} -
- - diff --git a/app/templates/macros.j2 b/app/templates/macros.j2 index a4b2fc1..e8030e7 100644 --- a/app/templates/macros.j2 +++ b/app/templates/macros.j2 @@ -1,7 +1,7 @@ -{% macro court_button(id, status) -%} - +{% macro court_button(court) -%} + {%- endmacro %} {% macro player_button(player) -%} @@ -34,17 +34,17 @@ {% macro header(url_for) -%} - - - Match Up! - - - - - - - - + + +Match Up! + + + + + + + + {%- endmacro %} diff --git a/app/utilities.py b/app/utilities.py index cd75d2a..d8bd3da 100644 --- a/app/utilities.py +++ b/app/utilities.py @@ -41,7 +41,7 @@ def select_players(players: Sequence[Player], count: int) -> list[Player]: player_identifiers = [] for game in games: for i in range(4): - player_identifiers.append(getattr(game, f"player_{i+1}")) + 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) @@ -89,10 +89,11 @@ def separate_teams_and_players( 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.") - player_data = {"round_id": None} | {f"player_{i+1}": p.id for i in range(4)} + 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))) for court in courts # type: ignore + DisplayGame(Game(**({"court_id": court.id} | player_data))) # type: ignore + for court in courts ] return games -- cgit v1.3.1