summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaran Jayachandra <mail@karanjayachandra.com>2025-07-14 20:16:07 +0200
committerKaran Jayachandra <mail@karanjayachandra.com>2025-07-14 20:16:07 +0200
commitb73e8f510f1b3a13d942e1ad42c8a04401eb6b8c (patch)
tree26612c20e03adb3cb26c36f36d31576f3c77dc1c
parent072d546490ba6cd4422cbc9cd6644b455f58fd46 (diff)
parent895ed4be433a4159250b276bd99f574c9c39c98e (diff)
Merge branch 'develop' into 'main'3.0
Develop See merge request KaranJayachandra/match_up!17
-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
-rw-r--r--migrations/env.py31
-rw-r--r--migrations/versions/c08245a9c93f_.py147
-rw-r--r--migrations/versions/d158cf8f4950_.py26
-rw-r--r--pyproject.toml2
-rw-r--r--uv.lock45
17 files changed, 371 insertions, 282 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
diff --git a/migrations/env.py b/migrations/env.py
index 4c97092..d004741 100644
--- a/migrations/env.py
+++ b/migrations/env.py
@@ -12,32 +12,31 @@ config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
-logger = logging.getLogger('alembic.env')
+logger = logging.getLogger("alembic.env")
def get_engine():
try:
# this works with Flask-SQLAlchemy<3 and Alchemical
- return current_app.extensions['migrate'].db.get_engine()
+ return current_app.extensions["migrate"].db.get_engine()
except (TypeError, AttributeError):
# this works with Flask-SQLAlchemy>=3
- return current_app.extensions['migrate'].db.engine
+ return current_app.extensions["migrate"].db.engine
def get_engine_url():
try:
- return get_engine().url.render_as_string(hide_password=False).replace(
- '%', '%%')
+ return get_engine().url.render_as_string(hide_password=False).replace("%", "%%")
except AttributeError:
- return str(get_engine().url).replace('%', '%%')
+ return str(get_engine().url).replace("%", "%%")
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
-config.set_main_option('sqlalchemy.url', get_engine_url())
-target_db = current_app.extensions['migrate'].db
+config.set_main_option("sqlalchemy.url", get_engine_url())
+target_db = current_app.extensions["migrate"].db
# other values from the config, defined by the needs of env.py,
# can be acquired:
@@ -46,7 +45,7 @@ target_db = current_app.extensions['migrate'].db
def get_metadata():
- if hasattr(target_db, 'metadatas'):
+ if hasattr(target_db, "metadatas"):
return target_db.metadatas[None]
return target_db.metadata
@@ -64,9 +63,7 @@ def run_migrations_offline():
"""
url = config.get_main_option("sqlalchemy.url")
- context.configure(
- url=url, target_metadata=get_metadata(), literal_binds=True
- )
+ context.configure(url=url, target_metadata=get_metadata(), literal_binds=True)
with context.begin_transaction():
context.run_migrations()
@@ -84,13 +81,13 @@ def run_migrations_online():
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
- if getattr(config.cmd_opts, 'autogenerate', False):
+ if getattr(config.cmd_opts, "autogenerate", False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
- logger.info('No changes in schema detected.')
+ logger.info("No changes in schema detected.")
- conf_args = current_app.extensions['migrate'].configure_args
+ conf_args = current_app.extensions["migrate"].configure_args
if conf_args.get("process_revision_directives") is None:
conf_args["process_revision_directives"] = process_revision_directives
@@ -98,9 +95,7 @@ def run_migrations_online():
with connectable.connect() as connection:
context.configure(
- connection=connection,
- target_metadata=get_metadata(),
- **conf_args
+ connection=connection, target_metadata=get_metadata(), **conf_args
)
with context.begin_transaction():
diff --git a/migrations/versions/c08245a9c93f_.py b/migrations/versions/c08245a9c93f_.py
index 5755bcc..48b115b 100644
--- a/migrations/versions/c08245a9c93f_.py
+++ b/migrations/versions/c08245a9c93f_.py
@@ -5,83 +5,118 @@ Revises: d158cf8f4950
Create Date: 2025-07-12 21:27:46.932139
"""
+
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
-revision = 'c08245a9c93f'
-down_revision = 'd158cf8f4950'
+revision = "c08245a9c93f"
+down_revision = "d158cf8f4950"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
- op.create_table('court',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('status', sa.Boolean(), nullable=False),
- sa.PrimaryKeyConstraint('id')
+ op.create_table(
+ "court",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("status", sa.Boolean(), nullable=False),
+ sa.PrimaryKeyConstraint("id"),
)
- op.create_table('player',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('first_name', sa.String(length=64), nullable=False),
- sa.Column('last_name', sa.String(length=64), nullable=False),
- sa.Column('team', sa.Integer(), nullable=False),
- sa.Column('status', sa.Boolean(), nullable=False),
- sa.Column('level', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
+ op.create_table(
+ "player",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("first_name", sa.String(length=64), nullable=False),
+ sa.Column("last_name", sa.String(length=64), nullable=False),
+ sa.Column("team", sa.Integer(), nullable=False),
+ sa.Column("status", sa.Boolean(), nullable=False),
+ sa.Column("level", sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint("id"),
)
- with op.batch_alter_table('player', schema=None) as batch_op:
- batch_op.create_index(batch_op.f('ix_player_first_name'), ['first_name'], unique=False)
- batch_op.create_index(batch_op.f('ix_player_last_name'), ['last_name'], unique=False)
- batch_op.create_index(batch_op.f('ix_player_team'), ['team'], unique=False)
+ with op.batch_alter_table("player", schema=None) as batch_op:
+ batch_op.create_index(
+ batch_op.f("ix_player_first_name"), ["first_name"], unique=False
+ )
+ batch_op.create_index(
+ batch_op.f("ix_player_last_name"), ["last_name"], unique=False
+ )
+ batch_op.create_index(batch_op.f("ix_player_team"), ["team"], unique=False)
- op.create_table('game',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('round_id', sa.Integer(), nullable=False),
- sa.Column('court_id', sa.Integer(), nullable=False),
- sa.Column('player_1', sa.Integer(), nullable=False),
- sa.Column('player_2', sa.Integer(), nullable=False),
- sa.Column('player_3', sa.Integer(), nullable=False),
- sa.Column('player_4', sa.Integer(), nullable=False),
- sa.Column('date', sa.DateTime(), nullable=False),
- sa.ForeignKeyConstraint(['court_id'], ['court.id'], ),
- sa.ForeignKeyConstraint(['player_1'], ['player.id'], ),
- sa.ForeignKeyConstraint(['player_2'], ['player.id'], ),
- sa.ForeignKeyConstraint(['player_3'], ['player.id'], ),
- sa.ForeignKeyConstraint(['player_4'], ['player.id'], ),
- sa.PrimaryKeyConstraint('id')
+ op.create_table(
+ "game",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("round_id", sa.Integer(), nullable=False),
+ sa.Column("court_id", sa.Integer(), nullable=False),
+ sa.Column("player_1", sa.Integer(), nullable=False),
+ sa.Column("player_2", sa.Integer(), nullable=False),
+ sa.Column("player_3", sa.Integer(), nullable=False),
+ sa.Column("player_4", sa.Integer(), nullable=False),
+ sa.Column("date", sa.DateTime(), nullable=False),
+ sa.ForeignKeyConstraint(
+ ["court_id"],
+ ["court.id"],
+ ),
+ sa.ForeignKeyConstraint(
+ ["player_1"],
+ ["player.id"],
+ ),
+ sa.ForeignKeyConstraint(
+ ["player_2"],
+ ["player.id"],
+ ),
+ sa.ForeignKeyConstraint(
+ ["player_3"],
+ ["player.id"],
+ ),
+ sa.ForeignKeyConstraint(
+ ["player_4"],
+ ["player.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
)
- with op.batch_alter_table('game', schema=None) as batch_op:
- batch_op.create_index(batch_op.f('ix_game_court_id'), ['court_id'], unique=False)
- batch_op.create_index(batch_op.f('ix_game_date'), ['date'], unique=False)
- batch_op.create_index(batch_op.f('ix_game_player_1'), ['player_1'], unique=False)
- batch_op.create_index(batch_op.f('ix_game_player_2'), ['player_2'], unique=False)
- batch_op.create_index(batch_op.f('ix_game_player_3'), ['player_3'], unique=False)
- batch_op.create_index(batch_op.f('ix_game_player_4'), ['player_4'], unique=False)
- batch_op.create_index(batch_op.f('ix_game_round_id'), ['round_id'], unique=False)
+ with op.batch_alter_table("game", schema=None) as batch_op:
+ batch_op.create_index(
+ batch_op.f("ix_game_court_id"), ["court_id"], unique=False
+ )
+ batch_op.create_index(batch_op.f("ix_game_date"), ["date"], unique=False)
+ batch_op.create_index(
+ batch_op.f("ix_game_player_1"), ["player_1"], unique=False
+ )
+ batch_op.create_index(
+ batch_op.f("ix_game_player_2"), ["player_2"], unique=False
+ )
+ batch_op.create_index(
+ batch_op.f("ix_game_player_3"), ["player_3"], unique=False
+ )
+ batch_op.create_index(
+ batch_op.f("ix_game_player_4"), ["player_4"], unique=False
+ )
+ batch_op.create_index(
+ batch_op.f("ix_game_round_id"), ["round_id"], unique=False
+ )
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
- with op.batch_alter_table('game', schema=None) as batch_op:
- batch_op.drop_index(batch_op.f('ix_game_round_id'))
- batch_op.drop_index(batch_op.f('ix_game_player_4'))
- batch_op.drop_index(batch_op.f('ix_game_player_3'))
- batch_op.drop_index(batch_op.f('ix_game_player_2'))
- batch_op.drop_index(batch_op.f('ix_game_player_1'))
- batch_op.drop_index(batch_op.f('ix_game_date'))
- batch_op.drop_index(batch_op.f('ix_game_court_id'))
+ with op.batch_alter_table("game", schema=None) as batch_op:
+ batch_op.drop_index(batch_op.f("ix_game_round_id"))
+ batch_op.drop_index(batch_op.f("ix_game_player_4"))
+ batch_op.drop_index(batch_op.f("ix_game_player_3"))
+ batch_op.drop_index(batch_op.f("ix_game_player_2"))
+ batch_op.drop_index(batch_op.f("ix_game_player_1"))
+ batch_op.drop_index(batch_op.f("ix_game_date"))
+ batch_op.drop_index(batch_op.f("ix_game_court_id"))
- op.drop_table('game')
- with op.batch_alter_table('player', schema=None) as batch_op:
- batch_op.drop_index(batch_op.f('ix_player_team'))
- batch_op.drop_index(batch_op.f('ix_player_last_name'))
- batch_op.drop_index(batch_op.f('ix_player_first_name'))
+ op.drop_table("game")
+ with op.batch_alter_table("player", schema=None) as batch_op:
+ batch_op.drop_index(batch_op.f("ix_player_team"))
+ batch_op.drop_index(batch_op.f("ix_player_last_name"))
+ batch_op.drop_index(batch_op.f("ix_player_first_name"))
- op.drop_table('player')
- op.drop_table('court')
+ op.drop_table("player")
+ op.drop_table("court")
# ### end Alembic commands ###
diff --git a/migrations/versions/d158cf8f4950_.py b/migrations/versions/d158cf8f4950_.py
index 392d3bf..ba0ef5b 100644
--- a/migrations/versions/d158cf8f4950_.py
+++ b/migrations/versions/d158cf8f4950_.py
@@ -1,16 +1,17 @@
"""empty message
Revision ID: d158cf8f4950
-Revises:
+Revises:
Create Date: 2025-07-06 15:31:24.124657
"""
+
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
-revision = 'd158cf8f4950'
+revision = "d158cf8f4950"
down_revision = None
branch_labels = None
depends_on = None
@@ -18,22 +19,23 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
- op.create_table('user',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('username', sa.String(length=64), nullable=False),
- sa.Column('password_hash', sa.String(length=256), nullable=True),
- sa.PrimaryKeyConstraint('id')
+ op.create_table(
+ "user",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("username", sa.String(length=64), nullable=False),
+ sa.Column("password_hash", sa.String(length=256), nullable=True),
+ sa.PrimaryKeyConstraint("id"),
)
- with op.batch_alter_table('user', schema=None) as batch_op:
- batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True)
+ with op.batch_alter_table("user", schema=None) as batch_op:
+ batch_op.create_index(batch_op.f("ix_user_username"), ["username"], unique=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
- with op.batch_alter_table('user', schema=None) as batch_op:
- batch_op.drop_index(batch_op.f('ix_user_username'))
+ with op.batch_alter_table("user", schema=None) as batch_op:
+ batch_op.drop_index(batch_op.f("ix_user_username"))
- op.drop_table('user')
+ op.drop_table("user")
# ### end Alembic commands ###
diff --git a/pyproject.toml b/pyproject.toml
index bbee86b..646335a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,9 @@ requires-python = ">=3.12"
dependencies = [
"flask>=3.1.1",
"flask-migrate>=4.1.0",
+ "flask-simplelogin>=0.2.0",
"flask-sqlalchemy>=3.1.1",
+ "flask-wtf>=1.2.2",
"gunicorn>=23.0.0",
"names>=0.3.0",
"pandas>=2.3.1",
diff --git a/uv.lock b/uv.lock
index de0471b..b084e7d 100644
--- a/uv.lock
+++ b/uv.lock
@@ -78,6 +78,21 @@ wheels = [
]
[[package]]
+name = "flask-simplelogin"
+version = "0.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "flask" },
+ { name = "flask-wtf" },
+ { name = "wtforms" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b4/f8/31d85485099f6b5ad214ec3f89c232d3605a77fefce7796e2a2e7b14e0e3/flask_simplelogin-0.2.0.tar.gz", hash = "sha256:bd8493dcb9e1a3657eff919cf95a8887bde4a861a579a55dd5ea9dd717ef983a", size = 7040, upload-time = "2024-10-30T18:21:03.015Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/96/dc/645e9fd39cd24a3d4c2279cd99faaa0bb50001184f458cf8a19eccec524f/flask_simplelogin-0.2.0-py3-none-any.whl", hash = "sha256:084f96bdd41a886b5bf07f23a1fa0479d8b224247967187a23190ee860909cb0", size = 7319, upload-time = "2024-10-30T18:21:01.544Z" },
+]
+
+[[package]]
name = "flask-sqlalchemy"
version = "3.1.1"
source = { registry = "https://pypi.org/simple" }
@@ -91,6 +106,20 @@ wheels = [
]
[[package]]
+name = "flask-wtf"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flask" },
+ { name = "itsdangerous" },
+ { name = "wtforms" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/80/9b/f1cd6e41bbf874f3436368f2c7ee3216c1e82d666ff90d1d800e20eb1317/flask_wtf-1.2.2.tar.gz", hash = "sha256:79d2ee1e436cf570bccb7d916533fa18757a2f18c290accffab1b9a0b684666b", size = 42641, upload-time = "2024-10-24T07:18:58.555Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/19/354449145fbebb65e7c621235b6ad69bebcfaec2142481f044d0ddc5b5c5/flask_wtf-1.2.2-py3-none-any.whl", hash = "sha256:e93160c5c5b6b571cf99300b6e01b72f9a101027cab1579901f8b10c5daf0b70", size = 12779, upload-time = "2024-10-24T07:18:56.976Z" },
+]
+
+[[package]]
name = "greenlet"
version = "3.2.3"
source = { registry = "https://pypi.org/simple" }
@@ -213,7 +242,9 @@ source = { virtual = "." }
dependencies = [
{ name = "flask" },
{ name = "flask-migrate" },
+ { name = "flask-simplelogin" },
{ name = "flask-sqlalchemy" },
+ { name = "flask-wtf" },
{ name = "gunicorn" },
{ name = "names" },
{ name = "pandas" },
@@ -224,7 +255,9 @@ dependencies = [
requires-dist = [
{ name = "flask", specifier = ">=3.1.1" },
{ name = "flask-migrate", specifier = ">=4.1.0" },
+ { name = "flask-simplelogin", specifier = ">=0.2.0" },
{ name = "flask-sqlalchemy", specifier = ">=3.1.1" },
+ { name = "flask-wtf", specifier = ">=1.2.2" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "names", specifier = ">=0.3.0" },
{ name = "pandas", specifier = ">=2.3.1" },
@@ -440,3 +473,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
]
+
+[[package]]
+name = "wtforms"
+version = "3.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/01/e4/633d080897e769ed5712dcfad626e55dbd6cf45db0ff4d9884315c6a82da/wtforms-3.2.1.tar.gz", hash = "sha256:df3e6b70f3192e92623128123ec8dca3067df9cfadd43d59681e210cfb8d4682", size = 137801, upload-time = "2024-10-21T11:34:00.108Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/08/c9/2088fb5645cd289c99ebe0d4cdcc723922a1d8e1beaefb0f6f76dff9b21c/wtforms-3.2.1-py3-none-any.whl", hash = "sha256:583bad77ba1dd7286463f21e11aa3043ca4869d03575921d1a1698d0715e0fd4", size = 152454, upload-time = "2024-10-21T11:33:58.44Z" },
+]