dev #4

Merged
nessi merged 25 commits from dev into main 2026-02-06 13:36:47 +00:00
Showing only changes of commit 8e5a2426e7 - Show all commits

View File

@@ -32,69 +32,210 @@ app.include_router(auth_router)
app.include_router(admin_router) app.include_router(admin_router)
app.include_router(games_router) app.include_router(games_router)
def _rand_join_code(n: int = 6) -> str: def _rand_join_code(n: int = 6) -> str:
# digits only (kahoot style) # digits only (kahoot style)
return "".join(random.choice(string.digits) for _ in range(n)) return "".join(random.choice(string.digits) for _ in range(n))
def _has_column(db: Session, table: str, col: str) -> bool:
"""
SQLite + Postgres friendly check.
We use a pragma first (SQLite), fallback to information_schema.
"""
try:
rows = db.execute(text(f"PRAGMA table_info({table})")).all()
return any(r[1] == col for r in rows) # pragma: column name is at index 1
except Exception:
db.rollback()
try:
rows = db.execute(
text(
"""
SELECT column_name
FROM information_schema.columns
WHERE table_name = :t AND column_name = :c
"""
),
{"t": table, "c": col},
).all()
return len(rows) > 0
except Exception:
db.rollback()
return False
def _auto_migrate(db: Session): def _auto_migrate(db: Session):
""" """
Very small, pragmatic auto-migration (no alembic). Very small, pragmatic auto-migration (no alembic).
- creates missing tables via create_all - creates missing tables via create_all
- adds missing columns via ALTER TABLE (best effort) - adds missing columns via ALTER TABLE (best effort)
- supports old schema (join_code/chip_code) and new schema (code/chip)
""" """
# Users.theme_key
# --- users.theme_key ---
if not _has_column(db, "users", "theme_key"):
try: try:
db.execute(text("ALTER TABLE users ADD COLUMN theme_key VARCHAR DEFAULT 'default'")) db.execute(text("ALTER TABLE users ADD COLUMN theme_key VARCHAR DEFAULT 'default'"))
db.commit() db.commit()
except Exception: except Exception:
db.rollback() db.rollback()
# Games.join_code + winner_user_id # --- games: code / join_code + winner_user_id + host_user_id (optional) ---
# We support both column names:
# old: join_code
# new: code
has_join_code = _has_column(db, "games", "join_code")
has_code = _has_column(db, "games", "code")
# If neither exists, create "code" (new preferred)
if not has_join_code and not has_code:
try: try:
db.execute(text("ALTER TABLE games ADD COLUMN join_code VARCHAR")) db.execute(text("ALTER TABLE games ADD COLUMN code VARCHAR"))
db.commit() db.commit()
has_code = True
except Exception: except Exception:
db.rollback() db.rollback()
# If only join_code exists but your code now expects "code",
# add "code" too and later mirror values.
if has_join_code and not has_code:
try:
db.execute(text("ALTER TABLE games ADD COLUMN code VARCHAR"))
db.commit()
has_code = True
except Exception:
db.rollback()
# winner_user_id
if not _has_column(db, "games", "winner_user_id"):
try: try:
db.execute(text("ALTER TABLE games ADD COLUMN winner_user_id VARCHAR")) db.execute(text("ALTER TABLE games ADD COLUMN winner_user_id VARCHAR"))
db.commit() db.commit()
except Exception: except Exception:
db.rollback() db.rollback()
# SheetState.chip_code # host_user_id (nice to have for "only host can set winner")
if not _has_column(db, "games", "host_user_id"):
try: try:
db.execute(text("ALTER TABLE sheet_state ADD COLUMN chip_code VARCHAR")) db.execute(text("ALTER TABLE games ADD COLUMN host_user_id VARCHAR"))
db.commit() db.commit()
except Exception: except Exception:
db.rollback() db.rollback()
# Ensure unique index for join_code (best effort) # --- sheet_state chip / chip_code ---
has_chip_code = _has_column(db, "sheet_state", "chip_code")
has_chip = _has_column(db, "sheet_state", "chip")
if not has_chip_code and not has_chip:
# prefer "chip"
try: try:
db.execute(text("ALTER TABLE sheet_state ADD COLUMN chip VARCHAR"))
db.commit()
has_chip = True
except Exception:
db.rollback()
# if old chip_code exists but new expects chip -> add chip and mirror later
if has_chip_code and not has_chip:
try:
db.execute(text("ALTER TABLE sheet_state ADD COLUMN chip VARCHAR"))
db.commit()
has_chip = True
except Exception:
db.rollback()
# --- indexes for game code ---
# We create unique index for the column(s) that exist.
try:
if has_join_code:
db.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS ix_games_join_code ON games (join_code)")) db.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS ix_games_join_code ON games (join_code)"))
if has_code:
db.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS ix_games_code ON games (code)"))
db.commit() db.commit()
except Exception: except Exception:
db.rollback() db.rollback()
# Backfill join_code for existing games # --- backfill code values ---
games = db.query(Game).filter((Game.join_code == None) | (Game.join_code == "")).all() # noqa: E711 # 1) if join_code exists and code exists, ensure code mirrors join_code where missing
if games: if has_join_code and has_code:
used = set([r[0] for r in db.execute(text("SELECT join_code FROM games WHERE join_code IS NOT NULL")).all() if r[0]]) try:
for g in games: db.execute(text("UPDATE games SET code = join_code WHERE (code IS NULL OR code = '') AND join_code IS NOT NULL AND join_code <> ''"))
db.commit()
except Exception:
db.rollback()
# 2) generate missing codes in whichever column we have
# Prefer writing into "code" (new), but also keep join_code in sync if present.
code_col = "code" if has_code else "join_code" if has_join_code else None
if code_col:
try:
missing = db.execute(
text(f"SELECT id FROM games WHERE {code_col} IS NULL OR {code_col} = ''")
).all()
except Exception:
db.rollback()
missing = []
if missing:
try:
used_rows = db.execute(text(f"SELECT {code_col} FROM games WHERE {code_col} IS NOT NULL")).all()
used = set([r[0] for r in used_rows if r and r[0]])
except Exception:
db.rollback()
used = set()
for (gid,) in missing:
code = _rand_join_code() code = _rand_join_code()
while code in used: while code in used:
code = _rand_join_code() code = _rand_join_code()
g.join_code = code
used.add(code) used.add(code)
db.commit()
# Backfill membership: ensure owner is member try:
# write into main col
db.execute(text(f"UPDATE games SET {code_col} = :c WHERE id = :id"), {"c": code, "id": gid})
# keep both in sync if both exist
if has_join_code and code_col == "code":
db.execute(text("UPDATE games SET join_code = :c WHERE id = :id AND (join_code IS NULL OR join_code = '')"), {"c": code, "id": gid})
if has_code and code_col == "join_code":
db.execute(text("UPDATE games SET code = :c WHERE id = :id AND (code IS NULL OR code = '')"), {"c": code, "id": gid})
db.commit()
except Exception:
db.rollback()
# --- backfill host_user_id: default to owner_user_id ---
try:
if _has_column(db, "games", "host_user_id"):
db.execute(text("UPDATE games SET host_user_id = owner_user_id WHERE host_user_id IS NULL OR host_user_id = ''"))
db.commit()
except Exception:
db.rollback()
# --- backfill membership: ensure owner is member ---
# uses ORM; only relies on existing table GameMember (create_all already ran)
try:
all_games = db.query(Game).all() all_games = db.query(Game).all()
for g in all_games: for g in all_games:
exists = db.query(GameMember).filter(GameMember.game_id == g.id, GameMember.user_id == g.owner_user_id).first() exists = (
db.query(GameMember)
.filter(GameMember.game_id == g.id, GameMember.user_id == g.owner_user_id)
.first()
)
if not exists: if not exists:
db.add(GameMember(game_id=g.id, user_id=g.owner_user_id)) db.add(GameMember(game_id=g.id, user_id=g.owner_user_id))
db.commit() db.commit()
except Exception:
db.rollback()
# --- mirror chip_code -> chip if both exist and chip empty ---
if has_chip_code and has_chip:
try:
db.execute(text("UPDATE sheet_state SET chip = chip_code WHERE (chip IS NULL OR chip = '') AND chip_code IS NOT NULL AND chip_code <> ''"))
db.commit()
except Exception:
db.rollback()
def seed_entries(db: Session): def seed_entries(db: Session):
if db.query(Entry).count() > 0: if db.query(Entry).count() > 0:
@@ -111,6 +252,7 @@ def seed_entries(db: Session):
db.add(Entry(category=Category.location.value, label=l)) db.add(Entry(category=Category.location.value, label=l))
db.commit() db.commit()
def ensure_admin(db: Session): def ensure_admin(db: Session):
admin_email = os.environ.get("ADMIN_EMAIL", "admin@local").lower().strip() admin_email = os.environ.get("ADMIN_EMAIL", "admin@local").lower().strip()
admin_pw = os.environ.get("ADMIN_PASSWORD", "ChangeMeNow123!") admin_pw = os.environ.get("ADMIN_PASSWORD", "ChangeMeNow123!")
@@ -126,6 +268,7 @@ def ensure_admin(db: Session):
) )
db.commit() db.commit()
@app.on_event("startup") @app.on_event("startup")
def on_startup(): def on_startup():
# create new tables # create new tables