aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/__init__.py132
-rw-r--r--app/config.py7
-rw-r--r--app/controller.py36
-rw-r--r--app/model.py8
-rw-r--r--app/routes.py111
-rw-r--r--app/static/custom.js10
-rw-r--r--app/templates/controls.j22
-rw-r--r--app/templates/index.j29
-rw-r--r--app/templates/login.html23
-rw-r--r--app/templates/login.j225
-rw-r--r--app/templates/macros.j230
-rw-r--r--app/utilities.py9
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