diff options
| author | Karan Jayachandra <mail@karanjayachandra.com> | 2025-07-14 19:51:45 +0200 |
|---|---|---|
| committer | Karan Jayachandra <mail@karanjayachandra.com> | 2025-07-14 19:51:45 +0200 |
| commit | 4aee68096c11ce1e3762fb8cffbde2a14dfa3cd0 (patch) | |
| tree | 4ce0642a088fd2370ad0992916d5d41093bedc48 /app | |
| parent | 0396e1fbb46c7256095f05e37f2747baa3e20c73 (diff) | |
Added the login for changes
Diffstat (limited to 'app')
| -rw-r--r-- | app/__init__.py | 132 | ||||
| -rw-r--r-- | app/config.py | 7 | ||||
| -rw-r--r-- | app/controller.py | 36 | ||||
| -rw-r--r-- | app/model.py | 8 | ||||
| -rw-r--r-- | app/routes.py | 111 | ||||
| -rw-r--r-- | app/static/custom.js | 10 | ||||
| -rw-r--r-- | app/templates/controls.j2 | 2 | ||||
| -rw-r--r-- | app/templates/index.j2 | 9 | ||||
| -rw-r--r-- | app/templates/login.html | 23 | ||||
| -rw-r--r-- | app/templates/login.j2 | 25 | ||||
| -rw-r--r-- | app/templates/macros.j2 | 30 | ||||
| -rw-r--r-- | app/utilities.py | 9 |
12 files changed, 206 insertions, 196 deletions
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/<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 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/<player_request>", 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/<court_number>", 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 @@ <div class="fixed-grid has-6-cols"> <div class="grid"> {% for court in courts %} - {{ court_button(court.id, court.status) }} + {{ court_button(court) }} {% endfor %} </div> </div> 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) }} <div hx-post="/clear" hx-swap="outerHTML" hx-trigger="load"></div> <div hx-get="/control" hx-swap="outerHTML" hx-trigger="load"></div> - {% with messages = get_flashed_messages() %} - {% if messages %} - <ul> - {% for message in messages %} - <li>{{ message }}</li> - {% endfor %} - </ul> - {% endif %} - {% endwith %} {{ footer() }} </section> </body> 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 %} +<!doctype html> +<html lang="en"> +{{ header(url_for) }} +<body> + <section class="section"> + {{ navbar(url_for) }} + <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> + {{ footer() }} + </section> +</body> + +</html>
\ 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 %} -<!doctype html> -<html lang="en"> - {{ header(url_for) }} - <body> - <section class="section"> - {{ navbar(url_for) }} - <h1>Sign In</h1> - <form action="" method="post" novalidate> - {{ form.hidden_tag() }} - <p> - {{ form.username.label }}<br> - {{ form.username(size=32) }} - </p> - <p> - {{ form.password.label }}<br> - {{ form.password(size=32) }} - </p> - <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p> - <p>{{ form.submit() }}</p> - </form> - {{ footer() }} - </section> - </body> -</html> 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) -%} -<button class= "button is-responsive {% if status %}is-success{% else %}is-danger{% endif %}" - hx-post="{{ "/court-toggle/" ~ id}}" - hx-swap="outerHTML"> Court {{ id }}</button> +{% macro court_button(court) -%} +<button class= "button is-responsive {% if court.status %}is-success{% else %}is-danger{% endif %}" + hx-post="{{ "/court-toggle/" ~ court.id}}" + hx-swap="outerHTML"> Court {{ court.id }}</button> {%- endmacro %} {% macro player_button(player) -%} @@ -34,17 +34,17 @@ {% 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="{{ url_for('static', filename='bulma.min.css') }}"> - <link rel="stylesheet" href="{{ url_for('static', filename='notyf.min.css') }}"> - <link rel="stylesheet" href="{{ url_for('static', filename='custom.css') }}"> - <script src="{{ url_for('static', filename='htmx.min.js') }}"></script> - <script src="{{ url_for('static', filename='notyf.min.js') }}"></script> - <script src="{{ url_for('static', filename='fontawesome.js') }}"></script> - <script defer src="{{ url_for('static', filename='custom.js') }}"></script> +<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="{{ url_for('static', filename='bulma.min.css') }}"> +<link rel="stylesheet" href="{{ url_for('static', filename='notyf.min.css') }}"> +<link rel="stylesheet" href="{{ url_for('static', filename='custom.css') }}"> +<script src="{{ url_for('static', filename='htmx.min.js') }}"></script> +<script src="{{ url_for('static', filename='notyf.min.js') }}"></script> +<script src="{{ url_for('static', filename='fontawesome.js') }}"></script> +<script defer src="{{ url_for('static', filename='custom.js') }}"></script> </head> {%- 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 |
