2.0+ public access setup
This commit is contained in:
1 parent
c891163c6d
commit
2eadc722e9
45 files changed
+7875
-32
No files matched your search
@@ -0,0 +1,324 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
import string
|
||||
from datetime import date
|
||||
|
||||
from auth import hash_password, normalize_email
|
||||
from 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
|
||||
LedgerEvent,
|
||||
League,
|
||||
LeagueMembership,
|
||||
Player,
|
||||
PokerSession,
|
||||
Season,
|
||||
User,
|
||||
make_player,
|
||||
make_session,
|
||||
make_user,
|
||||
normalize_lookup,
|
||||
)
|
||||
from utils import slugify
|
||||
|
||||
PUBLIC_KEY_ALPHABET = string.ascii_lowercase + string.digits
|
||||
|
||||
|
||||
def generate_public_key(length: int = 6) -> str:
|
||||
return "".join(secrets.choice(PUBLIC_KEY_ALPHABET) for _ in range(length))
|
||||
|
||||
|
||||
def unique_league_public_key() -> str:
|
||||
while True:
|
||||
public_key = generate_public_key()
|
||||
if League.query.filter_by(public_key=public_key).first() is None:
|
||||
return public_key
|
||||
|
||||
|
||||
def create_user(email: str, password: str) -> User:
|
||||
user = make_user(normalize_email(email), hash_password(password))
|
||||
db.session.add(user)
|
||||
return user
|
||||
|
||||
|
||||
def create_league(owner: User, name: str, slug: str, description: str | None = None) -> League:
|
||||
league = League(
|
||||
name=name.strip(),
|
||||
slug=slug.strip().lower(),
|
||||
public_key=unique_league_public_key(),
|
||||
description=description.strip() if description else None,
|
||||
created_by_user_id=owner.id,
|
||||
)
|
||||
db.session.add(league)
|
||||
db.session.flush()
|
||||
|
||||
db.session.add(
|
||||
LeagueMembership(
|
||||
league_id=league.id,
|
||||
user_id=owner.id,
|
||||
role="owner",
|
||||
)
|
||||
)
|
||||
return league
|
||||
|
||||
|
||||
def unique_league_slug(name: str, requested_slug: str | None = None) -> str:
|
||||
return slugify(requested_slug or name)
|
||||
|
||||
|
||||
def find_user_by_email(email: str) -> User | None:
|
||||
return User.query.filter_by(email=normalize_email(email)).one_or_none()
|
||||
|
||||
|
||||
def find_league_by_slug(slug: str) -> League | None:
|
||||
return League.query.filter_by(slug=slug.strip().lower(), archived_at=None).one_or_none()
|
||||
|
||||
|
||||
def find_league_by_id(league_id: str) -> League | None:
|
||||
return League.query.filter_by(id=league_id, archived_at=None).one_or_none()
|
||||
|
||||
|
||||
def find_league_by_public_key(public_key: str) -> League | None:
|
||||
return League.query.filter_by(
|
||||
public_key=public_key.strip().lower(),
|
||||
archived_at=None,
|
||||
).one_or_none()
|
||||
|
||||
|
||||
def list_public_leagues(name_query: str = "") -> list[League]:
|
||||
query = League.query.filter_by(visibility="public", archived_at=None)
|
||||
if name_query:
|
||||
query = query.filter(League.name.ilike(f"%{name_query}%"))
|
||||
return query.order_by(League.name.asc()).all()
|
||||
|
||||
|
||||
def list_leagues_for_user(user_id: str) -> list[tuple[League, LeagueMembership]]:
|
||||
return (
|
||||
db.session.query(League, LeagueMembership)
|
||||
.join(LeagueMembership, LeagueMembership.league_id == League.id)
|
||||
.filter(
|
||||
LeagueMembership.user_id == user_id,
|
||||
LeagueMembership.disabled_at.is_(None),
|
||||
League.archived_at.is_(None),
|
||||
)
|
||||
.order_by(League.name.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def league_counts(league_id: str) -> dict[str, int]:
|
||||
return {
|
||||
"players": Player.query.filter_by(league_id=league_id, status="active").count(),
|
||||
"seasons": Season.query.filter_by(league_id=league_id, archived_at=None).count(),
|
||||
"sessions": PokerSession.query.filter_by(league_id=league_id).count(),
|
||||
"open_sessions": PokerSession.query.filter_by(league_id=league_id, status="open").count(),
|
||||
"events": LedgerEvent.query.filter_by(league_id=league_id, voided_at=None).count(),
|
||||
}
|
||||
|
||||
|
||||
def create_player(league_id: str, display_name: str, slug: str) -> Player:
|
||||
player = make_player(league_id=league_id, display_name=display_name, slug=slug)
|
||||
db.session.add(player)
|
||||
return player
|
||||
|
||||
|
||||
def unique_player_slug(league_id: str, display_name: str, requested_slug: str | None = None) -> str:
|
||||
base_slug = slugify(requested_slug or display_name, fallback="player")
|
||||
candidate = base_slug
|
||||
suffix = 2
|
||||
|
||||
while Player.query.filter_by(league_id=league_id, slug=candidate).first() is not None:
|
||||
candidate = f"{base_slug}-{suffix}"
|
||||
suffix += 1
|
||||
|
||||
return candidate
|
||||
|
||||
|
||||
def list_players_for_league(league_id: str) -> list[Player]:
|
||||
return (
|
||||
Player.query.filter_by(league_id=league_id)
|
||||
.order_by(Player.status.asc(), Player.display_name.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def find_player_for_league(league_id: str, player_id: str) -> Player | None:
|
||||
return Player.query.filter_by(league_id=league_id, id=player_id).one_or_none()
|
||||
|
||||
|
||||
def set_player_status(player: Player, status: str) -> Player:
|
||||
if status not in {"active", "archived"}:
|
||||
raise ValueError(f"Unsupported player status: {status}")
|
||||
|
||||
player.status = status
|
||||
return player
|
||||
|
||||
|
||||
def create_season(
|
||||
league_id: str,
|
||||
name: str,
|
||||
start_date: date | None = None,
|
||||
end_date: date | None = None,
|
||||
sort_order: int = 0,
|
||||
) -> Season:
|
||||
season = Season(
|
||||
league_id=league_id,
|
||||
name=name.strip(),
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
sort_order=sort_order,
|
||||
)
|
||||
db.session.add(season)
|
||||
return season
|
||||
|
||||
|
||||
def next_sequence_on_date(league_id: str, session_date: date) -> int:
|
||||
current = (
|
||||
db.session.query(db.func.max(PokerSession.sequence_on_date))
|
||||
.filter_by(league_id=league_id, session_date=session_date)
|
||||
.scalar()
|
||||
)
|
||||
return int(current or 0) + 1
|
||||
|
||||
|
||||
def create_poker_session(
|
||||
league_id: str,
|
||||
session_date: date,
|
||||
season_id: str | None = None,
|
||||
status: str = "closed",
|
||||
) -> PokerSession:
|
||||
session = make_session(
|
||||
league_id=league_id,
|
||||
season_id=season_id,
|
||||
session_date=session_date,
|
||||
sequence_on_date=next_sequence_on_date(league_id, session_date),
|
||||
status=status,
|
||||
)
|
||||
db.session.add(session)
|
||||
return session
|
||||
|
||||
|
||||
def list_sessions_for_league(league_id: str) -> list[PokerSession]:
|
||||
return (
|
||||
PokerSession.query.filter_by(league_id=league_id)
|
||||
.order_by(
|
||||
PokerSession.session_date.desc(),
|
||||
PokerSession.sequence_on_date.desc(),
|
||||
PokerSession.created_at.desc(),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def find_session_for_league(league_id: str, session_id: str) -> PokerSession | None:
|
||||
return PokerSession.query.filter_by(league_id=league_id, id=session_id).one_or_none()
|
||||
|
||||
|
||||
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
|
||||
|
||||
session.status = status
|
||||
if status == "open":
|
||||
session.opened_at = utc_now()
|
||||
session.closed_at = None
|
||||
else:
|
||||
session.closed_at = utc_now()
|
||||
return session
|
||||
|
||||
|
||||
def player_name_exists(league_id: str, display_name: str) -> bool:
|
||||
return (
|
||||
Player.query.filter_by(
|
||||
league_id=league_id,
|
||||
normalized_name=normalize_lookup(display_name),
|
||||
).first()
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
def player_name_taken(league_id: str, display_name: str, exclude_player_id: str) -> bool:
|
||||
return (
|
||||
Player.query.filter(
|
||||
Player.league_id == league_id,
|
||||
Player.normalized_name == normalize_lookup(display_name),
|
||||
Player.id != exclude_player_id,
|
||||
).first()
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
def update_player(player: Player, display_name: str, notes: str | None) -> Player:
|
||||
player.display_name = display_name.strip()
|
||||
player.normalized_name = normalize_lookup(display_name)
|
||||
player.notes = notes.strip() if notes else None
|
||||
return player
|
||||
|
||||
|
||||
def user_has_league_role(
|
||||
user_id: str,
|
||||
league_id: str,
|
||||
allowed_roles: set[str] | tuple[str, ...] | list[str],
|
||||
) -> bool:
|
||||
membership = LeagueMembership.query.filter_by(
|
||||
user_id=user_id,
|
||||
league_id=league_id,
|
||||
disabled_at=None,
|
||||
).one_or_none()
|
||||
return bool(membership and membership.role in set(allowed_roles))
|
||||
|
||||
|
||||
def find_membership(league_id: str, user_id: str) -> LeagueMembership | None:
|
||||
return LeagueMembership.query.filter_by(
|
||||
league_id=league_id,
|
||||
user_id=user_id,
|
||||
disabled_at=None,
|
||||
).one_or_none()
|
||||
|
||||
|
||||
def list_members_for_league(league_id: str) -> list[tuple[LeagueMembership, User]]:
|
||||
return (
|
||||
db.session.query(LeagueMembership, User)
|
||||
.join(User, User.id == LeagueMembership.user_id)
|
||||
.filter(
|
||||
LeagueMembership.league_id == league_id,
|
||||
LeagueMembership.disabled_at.is_(None),
|
||||
)
|
||||
.order_by(LeagueMembership.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def add_league_member(
|
||||
league_id: str,
|
||||
user_id: str,
|
||||
role: str,
|
||||
invited_by_user_id: str,
|
||||
) -> LeagueMembership:
|
||||
membership = LeagueMembership(
|
||||
league_id=league_id,
|
||||
user_id=user_id,
|
||||
role=role,
|
||||
invited_by_user_id=invited_by_user_id or None,
|
||||
)
|
||||
db.session.add(membership)
|
||||
return membership
|
||||
|
||||
|
||||
def remove_league_member(league_id: str, user_id: str) -> None:
|
||||
from db_models import utc_now
|
||||
|
||||
membership = LeagueMembership.query.filter_by(
|
||||
league_id=league_id,
|
||||
user_id=user_id,
|
||||
disabled_at=None,
|
||||
).one_or_none()
|
||||
if membership and membership.role != "owner":
|
||||
membership.disabled_at = utc_now()
|
||||
Reference in new issue
Block a user