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 ++- migrations/env.py | 31 ++++---- migrations/versions/c08245a9c93f_.py | 147 ++++++++++++++++++++++------------- migrations/versions/d158cf8f4950_.py | 26 ++++--- pyproject.toml | 2 + uv.lock | 45 +++++++++++ 17 files changed, 371 insertions(+), 282 deletions(-) delete mode 100644 app/routes.py create mode 100644 app/templates/login.html delete mode 100644 app/templates/login.j2 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 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 @@ -77,6 +77,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/c4/3f329b23d769fe7628a5fc57ad36956f1fb7132cf8837be6da762b197327/Flask_Migrate-4.1.0-py3-none-any.whl", hash = "sha256:24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d", size = 21237, upload-time = "2025-01-10T18:51:09.527Z" }, ] +[[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" @@ -90,6 +105,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/6a/89963a5c6ecf166e8be29e0d1bf6806051ee8fe6c82e232842e3aeac9204/flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0", size = 25125, upload-time = "2023-09-11T21:42:34.514Z" }, ] +[[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" @@ -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" }, +] -- cgit v1.3.1