Move application code into package

This commit is contained in:
SowinskiBraeden committed 2026-06-28 17:41:11 -07:00
1 parent 83c9fec489
commit e376b3628a
25 files changed
+285 -269

No files matched your search

+8
View File
@@ -23,6 +23,14 @@ The current app is account-based and league-based. League owners/managers record
- Chart.js
- plain CSS
## Project Layout
- `app.py` is the deployment entrypoint for `flask --app app` and Gunicorn.
- `boker/` contains the Flask application package, routes, models, services, repositories, and config.
- `templates/` and `static/` contain Jinja views and public assets.
- `migrations/` contains Alembic migrations for database deploys.
- `tests/` contains the committed regression suite.
## Ledger Model
Each ledger row is an event, not a final snapshot. Corrections are made by appending or voiding events, so the history remains auditable.
+2 -117
View File
@@ -1,125 +1,10 @@
#!/usr/bin/env python3
from __future__ import annotations
import click
import os
from flask import Flask, render_template
from boker.app import app, create_app
from auth import current_user_id, is_logged_in, is_site_admin, normalize_email
from config import DEFAULT_SECRET_KEY, Config, ProductionConfig
from db import database_extensions_available, db, init_database
from extensions import csrf, limiter, mail
from routes.account import account_bp
from routes.internal import internal_bp
from routes.leagues import leagues_bp
from routes.public import public_bp
from storage import ensure_data_file
from utils import cents_to_dollars, safe_date_label
__all__ = ["app", "create_app"]
def _config_for_environment():
app_env = os.getenv("APP_ENV", os.getenv("FLASK_ENV", "")).lower()
if app_env in {"prod", "production"}:
return ProductionConfig
return Config
def create_app(config_overrides: dict | None = None) -> Flask:
app = Flask(__name__)
app.config.from_object(_config_for_environment())
if config_overrides:
app.config.update(config_overrides)
if app.config["SESSION_COOKIE_SECURE"] and app.config["SECRET_KEY"] == DEFAULT_SECRET_KEY:
raise RuntimeError("Set SECRET_KEY before running in production.")
ensure_data_file(app.config["DATA_PATH"])
init_database(app)
csrf.init_app(app)
limiter.init_app(app)
mail.init_app(app)
app.jinja_env.filters["money"] = cents_to_dollars
app.jinja_env.filters["pretty_date"] = safe_date_label
@app.context_processor
def inject_globals() -> dict:
return {
"app_version": app.config["APP_VERSION"],
"current_user_id": current_user_id(),
"is_logged_in": is_logged_in(),
"current_user_is_site_admin": is_site_admin(),
}
app.register_blueprint(public_bp)
app.register_blueprint(account_bp)
app.register_blueprint(internal_bp)
app.register_blueprint(leagues_bp)
@app.errorhandler(404)
def not_found(e):
return render_template("404.html"), 404
@app.errorhandler(403)
def forbidden(e):
return render_template("403.html"), 403
@app.cli.command("init-db")
def init_db_command() -> None:
if not database_extensions_available() or db is None:
raise click.ClickException(
"Database dependencies are not installed. Run pip install -r requirements.txt."
)
with app.app_context():
db.create_all()
click.echo("Initialized database tables.")
@app.cli.command("grant-site-admin")
@click.argument("email")
def grant_site_admin_command(email: str) -> None:
if not database_extensions_available() or db is None:
raise click.ClickException(
"Database dependencies are not installed. Run pip install -r requirements.txt."
)
from db_models import User
normalized_email = normalize_email(email)
with app.app_context():
user = User.query.filter_by(email=normalized_email, disabled_at=None).one_or_none()
if user is None:
raise click.ClickException(f"No active user found for {normalized_email}.")
user.is_site_admin = True
db.session.commit()
click.echo(f"Granted site admin access to {normalized_email}.")
@app.cli.command("revoke-site-admin")
@click.argument("email")
def revoke_site_admin_command(email: str) -> None:
if not database_extensions_available() or db is None:
raise click.ClickException(
"Database dependencies are not installed. Run pip install -r requirements.txt."
)
from db_models import User
normalized_email = normalize_email(email)
with app.app_context():
user = User.query.filter_by(email=normalized_email).one_or_none()
if user is None:
raise click.ClickException(f"No user found for {normalized_email}.")
user.is_site_admin = False
db.session.commit()
click.echo(f"Revoked site admin access from {normalized_email}.")
return app
app = create_app()
if __name__ == "__main__":
app.run(debug=True)
+1
View File
@@ -0,0 +1 @@
"""Application package for myboker.org."""
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
from __future__ import annotations
import click
import os
from flask import Flask, render_template
from boker.auth import current_user_id, is_logged_in, is_site_admin, normalize_email
from boker.config import DEFAULT_SECRET_KEY, Config, ProductionConfig
from boker.db import database_extensions_available, db, init_database
from boker.extensions import csrf, limiter, mail
from boker.routes.account import account_bp
from boker.routes.internal import internal_bp
from boker.routes.leagues import leagues_bp
from boker.routes.public import public_bp
from boker.storage import ensure_data_file
from boker.utils import cents_to_dollars, safe_date_label
def _config_for_environment():
app_env = os.getenv("APP_ENV", os.getenv("FLASK_ENV", "")).lower()
if app_env in {"prod", "production"}:
return ProductionConfig
return Config
def create_app(config_overrides: dict | None = None) -> Flask:
app = Flask(__name__, template_folder="../templates", static_folder="../static")
app.config.from_object(_config_for_environment())
if config_overrides:
app.config.update(config_overrides)
if app.config["SESSION_COOKIE_SECURE"] and app.config["SECRET_KEY"] == DEFAULT_SECRET_KEY:
raise RuntimeError("Set SECRET_KEY before running in production.")
ensure_data_file(app.config["DATA_PATH"])
init_database(app)
csrf.init_app(app)
limiter.init_app(app)
mail.init_app(app)
app.jinja_env.filters["money"] = cents_to_dollars
app.jinja_env.filters["pretty_date"] = safe_date_label
@app.context_processor
def inject_globals() -> dict:
return {
"app_version": app.config["APP_VERSION"],
"current_user_id": current_user_id(),
"is_logged_in": is_logged_in(),
"current_user_is_site_admin": is_site_admin(),
}
app.register_blueprint(public_bp)
app.register_blueprint(account_bp)
app.register_blueprint(internal_bp)
app.register_blueprint(leagues_bp)
@app.errorhandler(404)
def not_found(e):
return render_template("404.html"), 404
@app.errorhandler(403)
def forbidden(e):
return render_template("403.html"), 403
@app.cli.command("init-db")
def init_db_command() -> None:
if not database_extensions_available() or db is None:
raise click.ClickException(
"Database dependencies are not installed. Run pip install -r requirements.txt."
)
with app.app_context():
db.create_all()
click.echo("Initialized database tables.")
@app.cli.command("grant-site-admin")
@click.argument("email")
def grant_site_admin_command(email: str) -> None:
if not database_extensions_available() or db is None:
raise click.ClickException(
"Database dependencies are not installed. Run pip install -r requirements.txt."
)
from boker.db_models import User
normalized_email = normalize_email(email)
with app.app_context():
user = User.query.filter_by(email=normalized_email, disabled_at=None).one_or_none()
if user is None:
raise click.ClickException(f"No active user found for {normalized_email}.")
user.is_site_admin = True
db.session.commit()
click.echo(f"Granted site admin access to {normalized_email}.")
@app.cli.command("revoke-site-admin")
@click.argument("email")
def revoke_site_admin_command(email: str) -> None:
if not database_extensions_available() or db is None:
raise click.ClickException(
"Database dependencies are not installed. Run pip install -r requirements.txt."
)
from boker.db_models import User
normalized_email = normalize_email(email)
with app.app_context():
user = User.query.filter_by(email=normalized_email).one_or_none()
if user is None:
raise click.ClickException(f"No user found for {normalized_email}.")
user.is_site_admin = False
db.session.commit()
click.echo(f"Revoked site admin access from {normalized_email}.")
return app
app = create_app()
+2 -2
View File
@@ -72,12 +72,12 @@ def is_site_admin() -> bool:
if not user_id:
return False
from db import database_extensions_available
from boker.db import database_extensions_available
if not database_extensions_available():
return False
from db_models import User
from boker.db_models import User
user = User.query.filter_by(id=user_id, disabled_at=None).one_or_none()
return bool(user and user.is_site_admin)
+2 -2
View File
@@ -4,8 +4,8 @@ from __future__ import annotations
from typing import Any
from models import SessionEntry, SessionSummary
from utils import net_result_bucket, session_chart_label, session_sort_key
from boker.models import SessionEntry, SessionSummary
from boker.utils import net_result_bucket, session_chart_label, session_sort_key
PLAYER_PALETTE = [
"#9b8cf0", # --line-1 violet
+1 -1
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
BASE_DIR = Path(__file__).resolve().parent.parent
DATA_PATH = BASE_DIR / "data" / "entries.csv"
DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}"
DEFAULT_SECRET_KEY = "change-this-before-deploying"
+1 -1
View File
@@ -24,7 +24,7 @@ def init_database(app: Any) -> bool:
app.config["DATABASE_EXTENSIONS_AVAILABLE"] = False
return False
import db_models # noqa: F401 - registers SQLAlchemy models with metadata.
import boker.db_models # noqa: F401 - registers SQLAlchemy models with metadata.
db.init_app(app)
migrate.init_app(app, db)
+1 -1
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import uuid
from datetime import date, datetime, timezone
from db import db
from boker.db import db
if db is None: # pragma: no cover - imported only when DB dependencies exist.
raise RuntimeError("Database dependencies are not installed.")
+1 -1
View File
@@ -7,7 +7,7 @@ import socket
from flask import current_app
from flask_mail import Message
from extensions import mail
from boker.extensions import mail
def _mail_send_suppressed() -> bool:
File renamed without changes.
@@ -5,13 +5,13 @@ import secrets
import string
from datetime import date
from auth import hash_password, normalize_email
from db import db
from boker.auth import hash_password, normalize_email
from boker.db import db
if db is None: # pragma: no cover - imported only when DB dependencies exist.
raise RuntimeError("Database dependencies are not installed.")
from db_models import ( # noqa: E402
from boker.db_models import ( # noqa: E402
LedgerEvent,
League,
LeagueMembership,
@@ -24,7 +24,7 @@ from db_models import ( # noqa: E402
make_user,
normalize_lookup,
)
from utils import slugify
from boker.utils import slugify
PUBLIC_KEY_ALPHABET = string.ascii_lowercase + string.digits
@@ -223,7 +223,7 @@ def set_session_status(session: PokerSession, status: str) -> PokerSession:
if status not in {"open", "closed"}:
raise ValueError(f"Unsupported session status: {status}")
from db_models import utc_now
from boker.db_models import utc_now
session.status = status
if status == "open":
@@ -323,7 +323,7 @@ def add_league_member(
def remove_league_member(league_id: str, user_id: str) -> None:
from db_models import utc_now
from boker.db_models import utc_now
membership = LeagueMembership.query.filter_by(
league_id=league_id,
@@ -421,7 +421,7 @@ def delete_season(season: Season) -> None:
def delete_league(league: League) -> None:
from db_models import LedgerEvent, LeagueMembership, Player, PokerSession, Season
from boker.db_models import LedgerEvent, LeagueMembership, Player, PokerSession, Season
LedgerEvent.query.filter_by(league_id=league.id).delete(synchronize_session=False)
PokerSession.query.filter_by(league_id=league.id).delete(synchronize_session=False)
Player.query.filter_by(league_id=league.id).delete(synchronize_session=False)
@@ -1,12 +1,12 @@
#!/usr/bin/env python3
from __future__ import annotations
from db import db
from boker.db import db
if db is None: # pragma: no cover - imported only when DB dependencies exist.
raise RuntimeError("Database dependencies are not installed.")
from db_models import ( # noqa: E402
from boker.db_models import ( # noqa: E402
CANONICAL_EVENT_TYPES,
LedgerEvent,
Player,
@@ -14,7 +14,7 @@ from db_models import ( # noqa: E402
User,
canonical_event_type,
)
from storage import EventRow # noqa: E402
from boker.storage import EventRow # noqa: E402
def session_event_ref(session: PokerSession) -> str:
View File
File renamed without changes.
File renamed without changes.
+21 -21
View File
@@ -7,7 +7,7 @@ from urllib.parse import urlsplit
from flask import Blueprint, flash, redirect, render_template, request, session as flask_session, url_for
from auth import (
from boker.auth import (
current_user_id,
generate_invite_token,
generate_reset_token,
@@ -20,8 +20,8 @@ from auth import (
verify_password,
verify_reset_token,
)
from db import database_extensions_available, db
from extensions import limiter
from boker.db import database_extensions_available, db
from boker.extensions import limiter
account_bp = Blueprint("account", __name__, url_prefix="/account")
EMAIL_VERIFICATION_TTL = timedelta(minutes=15)
@@ -64,7 +64,7 @@ def _new_verification_code() -> str:
def _issue_verification_code(user) -> str:
from db_models import utc_now
from boker.db_models import utc_now
code = _new_verification_code()
user.email_verification_code_hash = hash_password(code)
@@ -73,7 +73,7 @@ def _issue_verification_code(user) -> str:
def _send_verification_code(user) -> None:
from emails import send_email_verification_code
from boker.emails import send_email_verification_code
code = _issue_verification_code(user)
db.session.commit()
@@ -112,7 +112,7 @@ def register():
}
if request.method == "POST":
from league_repositories import create_user, find_user_by_email
from boker.league_repositories import create_user, find_user_by_email
email = normalize_email(form["email"])
password = request.form.get("password", "")
@@ -158,7 +158,7 @@ def login():
}
if request.method == "POST":
from league_repositories import find_user_by_email
from boker.league_repositories import find_user_by_email
user = find_user_by_email(form["email"])
password = request.form.get("password", "")
@@ -195,7 +195,7 @@ def verify_email():
flash("Account database is not available.", "error")
return redirect(url_for("account.login"))
from db_models import User, utc_now
from boker.db_models import User, utc_now
user_id = flask_session.get("pending_verification_user_id")
user = db.session.get(User, user_id) if user_id else None
@@ -245,7 +245,7 @@ def resend_verification_code():
flash("Account database is not available.", "error")
return redirect(url_for("account.login"))
from db_models import User
from boker.db_models import User
user_id = flask_session.get("pending_verification_user_id")
user = db.session.get(User, user_id) if user_id else None
@@ -275,7 +275,7 @@ def settings():
flash("Account database is not available.", "error")
return redirect(url_for("leagues.index"))
from db_models import User
from boker.db_models import User
user = db.session.get(User, current_user_id())
if user is None:
@@ -292,7 +292,7 @@ def update_email():
flash("Account database is not available.", "error")
return redirect(url_for("account.settings"))
from db_models import User
from boker.db_models import User
user = db.session.get(User, current_user_id())
if user is None:
@@ -307,7 +307,7 @@ def update_email():
elif not verify_password(user.password_hash, current_password):
flash("Current password is incorrect.", "error")
else:
from league_repositories import find_user_by_email
from boker.league_repositories import find_user_by_email
existing = find_user_by_email(new_email)
if existing and existing.id != user.id:
@@ -327,7 +327,7 @@ def update_password():
flash("Account database is not available.", "error")
return redirect(url_for("account.settings"))
from db_models import User
from boker.db_models import User
user = db.session.get(User, current_user_id())
if user is None:
@@ -359,7 +359,7 @@ def disable_account():
flash("Account database is not available.", "error")
return redirect(url_for("account.settings"))
from db_models import League, User, utc_now
from boker.db_models import League, User, utc_now
user = db.session.get(User, current_user_id())
if user is None:
@@ -395,8 +395,8 @@ def delete_account():
flash("Account database is not available.", "error")
return redirect(url_for("account.settings"))
from db_models import League, LeagueMembership, User, utc_now
from league_repositories import delete_league
from boker.db_models import League, LeagueMembership, User, utc_now
from boker.league_repositories import delete_league
user = db.session.get(User, current_user_id())
if user is None:
@@ -436,9 +436,9 @@ def forgot_password():
flash("Account database is not available.", "error")
return redirect(url_for("account.forgot_password"))
from emails import send_password_reset
from boker.emails import send_password_reset
from flask import current_app
from league_repositories import find_user_by_email
from boker.league_repositories import find_user_by_email
email = normalize_email(request.form.get("email", ""))
user = find_user_by_email(email)
@@ -472,7 +472,7 @@ def reset_password(token):
flash("Account database is not available.", "error")
return redirect(url_for("account.login"))
from db_models import User
from boker.db_models import User
user = db.session.get(User, user_id)
if user is None or user.disabled_at is not None:
@@ -507,8 +507,8 @@ def accept_invite(token):
flash("Account database is not available.", "error")
return redirect(url_for("leagues.index"))
from db_models import User
from league_repositories import add_league_member, find_league_by_id, find_membership, find_user_by_email
from boker.db_models import User
from boker.league_repositories import add_league_member, find_league_by_id, find_membership, find_user_by_email
invite_email = normalize_email(data.get("email", ""))
invite_next = url_for("account.accept_invite", token=token)
@@ -9,17 +9,17 @@ from flask import Blueprint, current_app, flash, redirect, render_template, requ
from sqlalchemy import or_
from sqlalchemy.exc import IntegrityError
from auth import (
from boker.auth import (
current_user_id,
generate_reset_token,
hash_password,
normalize_email,
site_admin_required,
)
from db import db
from db_models import LedgerEvent, League, LeagueMembership, Player, PokerSession, User, utc_now
from league_repositories import delete_league, transfer_league_ownership
from utils import slugify
from boker.db import db
from boker.db_models import LedgerEvent, League, LeagueMembership, Player, PokerSession, User, utc_now
from boker.league_repositories import delete_league, transfer_league_ownership
from boker.utils import slugify
internal_bp = Blueprint("internal", __name__, url_prefix="/internal")
@@ -415,7 +415,7 @@ def send_user_password_reset(user_id: str):
flash("Disabled users cannot receive password resets.", "error")
return redirect(url_for("internal.user_detail", user_id=user.id))
from emails import send_password_reset
from boker.emails import send_password_reset
token = generate_reset_token(user.id)
base_url = current_app.config.get("APP_BASE_URL", "").rstrip("/")
@@ -436,7 +436,7 @@ def create_user_temporary_password(user_id: str):
flash("Disabled users cannot receive temporary passwords.", "error")
return redirect(url_for("internal.user_detail", user_id=user.id))
from emails import send_temporary_password
from boker.emails import send_temporary_password
temporary_password = _temporary_password()
user.password_hash = hash_password(temporary_password)
+74 -74
View File
@@ -9,12 +9,12 @@ from datetime import datetime, timezone
from flask import Blueprint, abort, flash, make_response, redirect, render_template, request, url_for
from auth import current_user_id, login_required, normalize_email
from charts import cumulative_profit_series, player_session_series
from db import database_extensions_available, db
from models import SessionSummary
from services import apply_rank_changes, build_leaderboard, build_session_summaries, session_events
from utils import cents_to_dollars, session_label, session_sort_key
from boker.auth import current_user_id, login_required, normalize_email
from boker.charts import cumulative_profit_series, player_session_series
from boker.db import database_extensions_available, db
from boker.models import SessionSummary
from boker.services import apply_rank_changes, build_leaderboard, build_session_summaries, session_events
from boker.utils import cents_to_dollars, session_label, session_sort_key
leagues_bp = Blueprint("leagues", __name__)
@@ -48,7 +48,7 @@ def split_league_ref(league_ref: str) -> tuple[str, str]:
def require_league(league_ref: str, allowed_roles: set[str]):
from league_repositories import find_league_by_public_key, user_has_league_role
from boker.league_repositories import find_league_by_public_key, user_has_league_role
_slug, public_key = split_league_ref(league_ref)
league = find_league_by_public_key(public_key)
@@ -64,7 +64,7 @@ def require_league(league_ref: str, allowed_roles: set[str]):
def get_league_with_visibility_gate(league_ref: str):
"""Load league; if private, enforce login + membership. Returns (league, None) or (None, redirect)."""
from league_repositories import find_league_by_public_key, user_has_league_role
from boker.league_repositories import find_league_by_public_key, user_has_league_role
_slug, public_key = split_league_ref(league_ref)
league = find_league_by_public_key(public_key)
@@ -99,8 +99,8 @@ def empty_session_summary(session) -> SessionSummary:
def session_ref_map(league_id: str) -> dict[str, str]:
from ledger_repositories import session_event_ref
from league_repositories import list_sessions_for_league
from boker.ledger_repositories import session_event_ref
from boker.league_repositories import list_sessions_for_league
return {session_event_ref(session): session.id for session in list_sessions_for_league(league_id)}
@@ -112,8 +112,8 @@ def index():
flash("League database is not available.", "error")
return render_template("leagues_index.html", leagues=[])
from league_repositories import list_leagues_for_user
from ledger_repositories import list_event_rows_for_league
from boker.league_repositories import list_leagues_for_user
from boker.ledger_repositories import list_event_rows_for_league
memberships = list_leagues_for_user(current_user_id() or "")
league_summaries = []
@@ -190,8 +190,8 @@ def new():
}
if request.method == "POST":
from db_models import User
from league_repositories import create_league, unique_league_slug
from boker.db_models import User
from boker.league_repositories import create_league, unique_league_slug
owner = db.session.get(User, current_user_id())
if owner is None:
@@ -221,8 +221,8 @@ def dashboard(league_ref: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import league_counts, user_has_league_role
from ledger_repositories import list_event_rows_for_league
from boker.league_repositories import league_counts, user_has_league_role
from boker.ledger_repositories import list_event_rows_for_league
league, resp = get_league_with_visibility_gate(league_ref)
if resp:
@@ -289,7 +289,7 @@ def legacy_dashboard_redirect(league_id: str, league_slug: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import find_league_by_id, user_has_league_role
from boker.league_repositories import find_league_by_id, user_has_league_role
league = find_league_by_id(league_id)
if league is None:
@@ -306,8 +306,8 @@ def leaderboard(league_ref: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from ledger_repositories import list_event_rows_for_league
from league_repositories import (
from boker.ledger_repositories import list_event_rows_for_league
from boker.league_repositories import (
list_players_for_league,
list_seasons_for_league,
user_has_league_role,
@@ -328,10 +328,10 @@ def leaderboard(league_ref: str):
selected_season_id = request.args.get("season", "").strip()
selected_season = None
if selected_season_id:
from league_repositories import find_season, list_sessions_for_league
from boker.league_repositories import find_season, list_sessions_for_league
selected_season = find_season(league.id, selected_season_id)
if selected_season:
from ledger_repositories import session_event_ref
from boker.ledger_repositories import session_event_ref
season_db_sessions = list_sessions_for_league(league.id)
season_refs = {
session_event_ref(s)
@@ -442,12 +442,12 @@ def ledger(league_ref: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from ledger_repositories import (
from boker.ledger_repositories import (
append_ledger_event,
list_all_event_rows_for_league,
list_event_rows_for_league,
)
from league_repositories import (
from boker.league_repositories import (
find_player_for_league,
find_session_for_league,
list_players_for_league,
@@ -579,7 +579,7 @@ def void_event(league_ref: str, event_id: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from ledger_repositories import void_ledger_event
from boker.ledger_repositories import void_ledger_event
league = require_league(league_ref, {"owner"})
reason = request.form.get("void_reason", "").strip()
@@ -602,8 +602,8 @@ def player_detail(league_ref: str, player_id: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from ledger_repositories import list_event_rows_for_league
from league_repositories import find_player_for_league, user_has_league_role
from boker.ledger_repositories import list_event_rows_for_league
from boker.league_repositories import find_player_for_league, user_has_league_role
league, resp = get_league_with_visibility_gate(league_ref)
if resp:
@@ -661,7 +661,7 @@ def players(league_ref: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import (
from boker.league_repositories import (
create_player,
list_players_for_league,
player_name_exists,
@@ -700,7 +700,7 @@ def players(league_ref: str):
flash("Player added.", "success")
return redirect(url_for("leagues.players", **league_url_values(league)))
from ledger_repositories import list_event_rows_for_league
from boker.ledger_repositories import list_event_rows_for_league
all_sessions = build_session_summaries(list_event_rows_for_league(league.id))
board = build_leaderboard(all_sessions, league.break_even_cents)
@@ -734,7 +734,7 @@ def update_player_status(league_ref: str, player_id: str, status: str, message:
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import find_player_for_league, set_player_status
from boker.league_repositories import find_player_for_league, set_player_status
league = require_league(league_ref, {"owner", "manager"})
player = find_player_for_league(league.id, player_id)
@@ -754,7 +754,7 @@ def edit_player(league_ref: str, player_id: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import find_player_for_league, player_name_taken, update_player
from boker.league_repositories import find_player_for_league, player_name_taken, update_player
league = require_league(league_ref, {"owner", "manager"})
player = find_player_for_league(league.id, player_id)
@@ -782,8 +782,8 @@ def sessions(league_ref: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from ledger_repositories import append_ledger_event
from league_repositories import (
from boker.ledger_repositories import append_ledger_event
from boker.league_repositories import (
create_poker_session,
list_seasons_for_league,
list_sessions_for_league,
@@ -816,10 +816,10 @@ def sessions(league_ref: str):
status = "closed" if form["status"] == "closed" else "open"
season_id = form["season_id"] or None
if season_id is None:
from league_repositories import auto_assign_sessions_to_seasons as _auto
from boker.league_repositories import auto_assign_sessions_to_seasons as _auto
# try auto-assign: create the session first, then let the
# function match it; we pass a temporary session date check inline
from league_repositories import list_seasons_for_league as _ls
from boker.league_repositories import list_seasons_for_league as _ls
eligible = [
s for s in _ls(league.id, include_archived=False)
if s.start_date and s.end_date
@@ -836,7 +836,7 @@ def sessions(league_ref: str):
session.label = form["label"] or None
session.notes = form["notes"] or None
if status == "open":
from league_repositories import set_session_status
from boker.league_repositories import set_session_status
set_session_status(session, "open")
@@ -854,8 +854,8 @@ def sessions(league_ref: str):
flash(f"Created {session.display_label}.", "success")
return redirect(url_for("leagues.sessions", **league_url_values(league)))
from db_models import LedgerEvent
from ledger_repositories import list_event_rows_for_league
from boker.db_models import LedgerEvent
from boker.ledger_repositories import list_event_rows_for_league
all_sessions = list_sessions_for_league(league.id)
summaries = build_session_summaries(list_event_rows_for_league(league.id))
@@ -908,8 +908,8 @@ def prune_empty_sessions(league_ref: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from db_models import LedgerEvent, PokerSession
from league_repositories import list_sessions_for_league
from boker.db_models import LedgerEvent, PokerSession
from boker.league_repositories import list_sessions_for_league
league = require_league(league_ref, {"owner", "manager"})
@@ -944,7 +944,7 @@ def delete_session(league_ref: str, session_id: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import find_session_for_league
from boker.league_repositories import find_session_for_league
league = require_league(league_ref, {"owner"})
session = find_session_for_league(league.id, session_id)
@@ -964,7 +964,7 @@ def edit_session(league_ref: str, session_id: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import find_session_for_league
from boker.league_repositories import find_session_for_league
league = require_league(league_ref, {"owner", "manager"})
session = find_session_for_league(league.id, session_id)
@@ -983,7 +983,7 @@ def edit_session(league_ref: str, session_id: str):
return redirect(url_for("leagues.session_detail", league_ref=league.url_ref, session_id=session_id))
if new_date != session.session_date:
from db_models import PokerSession as _PS
from boker.db_models import PokerSession as _PS
max_seq = db.session.query(db.func.max(_PS.sequence_on_date)).filter(
_PS.league_id == league.id,
_PS.session_date == new_date,
@@ -993,7 +993,7 @@ def edit_session(league_ref: str, session_id: str):
session.session_date = new_date
if new_season_id is not None:
from league_repositories import find_season
from boker.league_repositories import find_season
valid = find_season(league.id, new_season_id)
new_season_id = valid.id if valid else None
@@ -1012,8 +1012,8 @@ def session_detail(league_ref: str, session_id: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from ledger_repositories import append_ledger_event, list_all_event_rows_for_session, list_event_rows_for_session
from league_repositories import (
from boker.ledger_repositories import append_ledger_event, list_all_event_rows_for_session, list_event_rows_for_session
from boker.league_repositories import (
find_session_for_league,
list_players_for_league,
list_seasons_for_league,
@@ -1101,9 +1101,9 @@ def session_public_view(league_ref: str, session_id: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from charts import session_breakdown_series
from ledger_repositories import list_event_rows_for_league, list_event_rows_for_session
from league_repositories import find_league_by_public_key, find_season, find_session_for_league, list_sessions_for_league, user_has_league_role
from boker.charts import session_breakdown_series
from boker.ledger_repositories import list_event_rows_for_league, list_event_rows_for_session
from boker.league_repositories import find_league_by_public_key, find_season, find_session_for_league, list_sessions_for_league, user_has_league_role
_slug, public_key = split_league_ref(league_ref)
league = find_league_by_public_key(public_key)
@@ -1182,8 +1182,8 @@ def update_session_status(league_ref: str, session_id: str, status: str, message
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from ledger_repositories import append_ledger_event
from league_repositories import find_session_for_league, set_session_status
from boker.ledger_repositories import append_ledger_event
from boker.league_repositories import find_session_for_league, set_session_status
league = require_league(league_ref, {"owner", "manager"})
session = find_session_for_league(league.id, session_id)
@@ -1214,7 +1214,7 @@ def seasons(league_ref: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import (
from boker.league_repositories import (
create_season,
list_seasons_for_league,
user_has_league_role,
@@ -1283,7 +1283,7 @@ def auto_assign_seasons(league_ref: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import auto_assign_sessions_to_seasons
from boker.league_repositories import auto_assign_sessions_to_seasons
league = require_league(league_ref, {"owner", "manager"})
count = auto_assign_sessions_to_seasons(league.id)
@@ -1302,7 +1302,7 @@ def update_season(league_ref: str, season_id: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import find_season, update_season as repo_update_season
from boker.league_repositories import find_season, update_season as repo_update_season
league = require_league(league_ref, {"owner", "manager"})
season = find_season(league.id, season_id)
@@ -1338,7 +1338,7 @@ def archive_season(league_ref: str, season_id: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import archive_season as repo_archive, find_season
from boker.league_repositories import archive_season as repo_archive, find_season
league = require_league(league_ref, {"owner", "manager"})
season = find_season(league.id, season_id)
@@ -1359,7 +1359,7 @@ def unarchive_season(league_ref: str, season_id: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import find_season, unarchive_season as repo_unarchive
from boker.league_repositories import find_season, unarchive_season as repo_unarchive
league = require_league(league_ref, {"owner", "manager"})
season = find_season(league.id, season_id)
@@ -1380,7 +1380,7 @@ def delete_season(league_ref: str, season_id: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import delete_season as repo_delete, find_season
from boker.league_repositories import delete_season as repo_delete, find_season
league = require_league(league_ref, {"owner", "manager"})
season = find_season(league.id, season_id)
@@ -1437,7 +1437,7 @@ def league_settings(league_ref: str):
elif break_even_cents < 0 or break_even_cents > 10000:
flash("Break-even threshold must be between $0.00 and $100.00.", "error")
else:
from utils import slugify
from boker.utils import slugify
league.name = form["name"]
league.slug = slugify(form["name"])
@@ -1449,7 +1449,7 @@ def league_settings(league_ref: str):
flash("League settings saved.", "success")
return redirect(url_for("leagues.league_settings", league_ref=league.url_ref))
from league_repositories import list_members_for_league
from boker.league_repositories import list_members_for_league
members = list_members_for_league(league.id)
return render_template("league_settings.html", league=league, form=form, is_owner=True, can_manage=True, members=members)
@@ -1462,11 +1462,11 @@ def invite_member(league_ref: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from auth import generate_invite_token
from emails import send_league_invite
from boker.auth import generate_invite_token
from boker.emails import send_league_invite
from flask import current_app
from db_models import User
from league_repositories import find_membership, find_user_by_email
from boker.db_models import User
from boker.league_repositories import find_membership, find_user_by_email
league = require_league(league_ref, {"owner"})
email = normalize_email(request.form.get("email", ""))
@@ -1509,7 +1509,7 @@ def remove_member(league_ref: str, user_id: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import remove_league_member
from boker.league_repositories import remove_league_member
league = require_league(league_ref, {"owner"})
@@ -1530,7 +1530,7 @@ def update_member_role(league_ref: str, user_id: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import set_league_member_role
from boker.league_repositories import set_league_member_role
league = require_league(league_ref, {"owner"})
role = request.form.get("role", "").strip()
@@ -1560,7 +1560,7 @@ def transfer_ownership(league_ref: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import transfer_league_ownership
from boker.league_repositories import transfer_league_ownership
league = require_league(league_ref, {"owner"})
new_owner_user_id = request.form.get("new_owner_user_id", "").strip()
@@ -1588,7 +1588,7 @@ def archive_league(league_ref: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from db_models import utc_now
from boker.db_models import utc_now
league = require_league(league_ref, {"owner"})
confirm_name = request.form.get("confirm_name", "").strip()
@@ -1610,7 +1610,7 @@ def delete_league_route(league_ref: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from league_repositories import delete_league
from boker.league_repositories import delete_league
league = require_league(league_ref, {"owner"})
confirm_name = request.form.get("confirm_name", "").strip()
@@ -1633,8 +1633,8 @@ def export_ledger_csv(league_ref: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from ledger_repositories import list_event_rows_for_league
from storage import CSV_HEADERS
from boker.ledger_repositories import list_event_rows_for_league
from boker.storage import CSV_HEADERS
league = require_league(league_ref, {"owner", "manager"})
rows = list_event_rows_for_league(league.id)
@@ -1659,14 +1659,14 @@ def import_ledger_csv(league_ref: str):
flash("League database is not available.", "error")
return redirect(url_for("public.home"))
from db_models import CANONICAL_EVENT_TYPES, canonical_event_type
from ledger_repositories import (
from boker.db_models import CANONICAL_EVENT_TYPES, canonical_event_type
from boker.ledger_repositories import (
append_ledger_event,
list_ledger_events_for_league,
session_event_ref,
)
from league_repositories import create_player, list_players_for_league, list_sessions_for_league, unique_player_slug
from storage import CSV_HEADERS
from boker.league_repositories import create_player, list_players_for_league, list_sessions_for_league, unique_player_slug
from boker.storage import CSV_HEADERS
league = require_league(league_ref, {"owner", "manager"})
@@ -1700,7 +1700,7 @@ def import_ledger_csv(league_ref: str):
flash(f"CSV header mismatch. Expected: {', '.join(CSV_HEADERS)}", "error")
return redirect(url_for("leagues.ledger", **league_url_values(league)))
from db_models import PokerSession, make_session
from boker.db_models import PokerSession, make_session
sessions = list_sessions_for_league(league.id)
session_by_ref = {session_event_ref(s): s for s in sessions}
+8 -8
View File
@@ -4,16 +4,16 @@ from __future__ import annotations
from flask import Blueprint, flash, redirect, render_template, request, url_for
from flask import current_app
from charts import cumulative_profit_series, player_session_series, session_breakdown_series
from config import ELIGIBLE_MIN_SESSIONS
from services import (
from boker.charts import cumulative_profit_series, player_session_series, session_breakdown_series
from boker.config import ELIGIBLE_MIN_SESSIONS
from boker.services import (
apply_rank_changes,
build_leaderboard,
build_session_summaries,
session_events,
)
from storage import load_events
from utils import session_label, session_sort_key
from boker.storage import load_events
from boker.utils import session_label, session_sort_key
public_bp = Blueprint("public", __name__)
@@ -40,9 +40,9 @@ def terms():
@public_bp.get("/explore")
def explore():
from auth import is_logged_in
from db import database_extensions_available
from league_repositories import league_counts, list_public_leagues
from boker.auth import is_logged_in
from boker.db import database_extensions_available
from boker.league_repositories import league_counts, list_public_leagues
q = request.args.get("q", "").strip()
leagues = list_public_leagues(q) if database_extensions_available() else []
+3 -3
View File
@@ -4,9 +4,9 @@ from __future__ import annotations
from collections import defaultdict
from models import PlayerStats, SessionEntry, SessionSummary
from storage import EventRow
from utils import entry_sort_key, net_result_bucket, session_sort_key
from boker.models import PlayerStats, SessionEntry, SessionSummary
from boker.storage import EventRow
from boker.utils import entry_sort_key, net_result_bucket, session_sort_key
SESSION_MARKER_TYPES = {"session_open", "session_close"}
View File
File renamed without changes.
+1 -1
View File
@@ -7,7 +7,7 @@ import re
from markupsafe import Markup
from models import SessionEntry, SessionSummary
from boker.models import SessionEntry, SessionSummary
BREAK_EVEN_TOLERANCE_CENTS = 100
+3 -3
View File
@@ -1,13 +1,13 @@
import unittest
from charts import player_session_series
from services import (
from boker.charts import player_session_series
from boker.services import (
build_leaderboard,
build_session_summaries,
pending_payout_carry_items,
prunable_empty_session_ids,
)
from utils import session_sort_key
from boker.utils import session_sort_key
def cents(amount: float) -> int:
+15 -15
View File
@@ -4,8 +4,8 @@ from datetime import datetime, timedelta, timezone
from pathlib import Path
from app import create_app
from auth import hash_password
from db import db
from boker.auth import hash_password
from boker.db import db
class InternalAdminAccessTests(unittest.TestCase):
@@ -22,7 +22,7 @@ class InternalAdminAccessTests(unittest.TestCase):
self.client = self.app.test_client()
with self.app.app_context():
from db_models import League, LeagueMembership, User
from boker.db_models import League, LeagueMembership, User
db.create_all()
self.user = User(
@@ -118,7 +118,7 @@ class InternalAdminAccessTests(unittest.TestCase):
def test_admin_users_page_is_paginated(self):
with self.app.app_context():
from db_models import User
from boker.db_models import User
users = [
User(email=f"bulk{i:02d}@example.com", password_hash=hash_password("password123"))
@@ -136,7 +136,7 @@ class InternalAdminAccessTests(unittest.TestCase):
def test_admin_leagues_page_is_paginated(self):
with self.app.app_context():
from db_models import League
from boker.db_models import League
leagues = [
League(
@@ -160,8 +160,8 @@ class InternalAdminAccessTests(unittest.TestCase):
def test_session_growth_uses_session_date_not_created_at(self):
with self.app.app_context():
from db_models import PokerSession
from routes.internal import _growth_data
from boker.db_models import PokerSession
from boker.routes.internal import _growth_data
now = datetime.now(timezone.utc)
old_session = PokerSession(
@@ -196,7 +196,7 @@ class InternalAdminAccessTests(unittest.TestCase):
self.assertEqual(response.status_code, 403)
with self.app.app_context():
from db_models import User
from boker.db_models import User
user = db.session.get(User, self.user_id)
self.assertEqual(user.email, "viewer@example.com")
@@ -212,7 +212,7 @@ class InternalAdminAccessTests(unittest.TestCase):
self.assertEqual(response.status_code, 200)
with self.app.app_context():
from db_models import User
from boker.db_models import User
user = db.session.get(User, self.user_id)
self.assertEqual(user.email, "changed@example.com")
@@ -220,7 +220,7 @@ class InternalAdminAccessTests(unittest.TestCase):
def test_admin_delete_user_blocks_owned_leagues(self):
self.login_as(self.admin_id)
with self.app.app_context():
from db_models import League, LeagueMembership
from boker.db_models import League, LeagueMembership
owned_league = League(
name="Manager Owned Poker",
@@ -241,7 +241,7 @@ class InternalAdminAccessTests(unittest.TestCase):
self.assertEqual(response.status_code, 302)
with self.app.app_context():
from db_models import User
from boker.db_models import User
self.assertIsNotNone(db.session.get(User, self.manager_id))
@@ -251,7 +251,7 @@ class InternalAdminAccessTests(unittest.TestCase):
archive_response = self.client.post(f"/internal/leagues/{self.league_id}/archive")
self.assertEqual(archive_response.status_code, 302)
with self.app.app_context():
from db_models import League
from boker.db_models import League
league = db.session.get(League, self.league_id)
self.assertIsNotNone(league.archived_at)
@@ -259,7 +259,7 @@ class InternalAdminAccessTests(unittest.TestCase):
restore_response = self.client.post(f"/internal/leagues/{self.league_id}/restore")
self.assertEqual(restore_response.status_code, 302)
with self.app.app_context():
from db_models import League
from boker.db_models import League
league = db.session.get(League, self.league_id)
self.assertIsNone(league.archived_at)
@@ -280,7 +280,7 @@ class InternalAdminAccessTests(unittest.TestCase):
self.assertEqual(response.status_code, 302)
with self.app.app_context():
from db_models import League
from boker.db_models import League
league = db.session.get(League, self.league_id)
self.assertEqual(league.name, "Saturday Poker")
@@ -300,7 +300,7 @@ class InternalAdminAccessTests(unittest.TestCase):
self.assertEqual(response.status_code, 302)
with self.app.app_context():
from db_models import League, LeagueMembership
from boker.db_models import League, LeagueMembership
league = db.session.get(League, self.league_id)
self.assertEqual(league.created_by_user_id, self.manager_id)
+2 -2
View File
@@ -1,7 +1,7 @@
import unittest
from models import SessionEntry, SessionSummary
from services import build_leaderboard
from boker.models import SessionEntry, SessionSummary
from boker.services import build_leaderboard
class CashInAccountingTest(unittest.TestCase):