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() }}
+
+ {{ navbar(url_for) }}
+