diff --git a/backend/app/main.py b/backend/app/main.py index 1510220..a954e3d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -32,76 +32,217 @@ app.include_router(auth_router) app.include_router(admin_router) app.include_router(games_router) + def _rand_join_code(n: int = 6) -> str: # digits only (kahoot style) 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): """ Very small, pragmatic auto-migration (no alembic). - creates missing tables via create_all - adds missing columns via ALTER TABLE (best effort) - """ - # Users.theme_key +- supports old schema (join_code/chip_code) and new schema (code/chip) +""" + + # --- users.theme_key --- + if not _has_column(db, "users", "theme_key"): + try: + db.execute(text("ALTER TABLE users ADD COLUMN theme_key VARCHAR DEFAULT 'default'")) + db.commit() + except Exception: + db.rollback() + + # --- 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: + db.execute(text("ALTER TABLE games ADD COLUMN code VARCHAR")) + db.commit() + has_code = True + except Exception: + 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: + db.execute(text("ALTER TABLE games ADD COLUMN winner_user_id VARCHAR")) + db.commit() + except Exception: + db.rollback() + + # host_user_id (nice to have for "only host can set winner") + if not _has_column(db, "games", "host_user_id"): + try: + db.execute(text("ALTER TABLE games ADD COLUMN host_user_id VARCHAR")) + db.commit() + except Exception: + db.rollback() + + # --- 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: + 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: - db.execute(text("ALTER TABLE users ADD COLUMN theme_key VARCHAR DEFAULT 'default'")) + if has_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() except Exception: db.rollback() - # Games.join_code + winner_user_id - try: - db.execute(text("ALTER TABLE games ADD COLUMN join_code VARCHAR")) - db.commit() - except Exception: - db.rollback() + # --- backfill code values --- + # 1) if join_code exists and code exists, ensure code mirrors join_code where missing + if has_join_code and has_code: + try: + 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() - try: - db.execute(text("ALTER TABLE games ADD COLUMN winner_user_id VARCHAR")) - 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 = [] - # SheetState.chip_code - try: - db.execute(text("ALTER TABLE sheet_state ADD COLUMN chip_code VARCHAR")) - db.commit() - except Exception: - db.rollback() + 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() - # Ensure unique index for join_code (best effort) - try: - db.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS ix_games_join_code ON games (join_code)")) - db.commit() - except Exception: - db.rollback() - - # Backfill join_code for existing games - games = db.query(Game).filter((Game.join_code == None) | (Game.join_code == "")).all() # noqa: E711 - if games: - 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]]) - for g in games: - code = _rand_join_code() - while code in used: + for (gid,) in missing: code = _rand_join_code() - g.join_code = code - used.add(code) - db.commit() + while code in used: + code = _rand_join_code() + used.add(code) + + 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() + for g in all_games: + exists = ( + db.query(GameMember) + .filter(GameMember.game_id == g.id, GameMember.user_id == g.owner_user_id) + .first() + ) + if not exists: + db.add(GameMember(game_id=g.id, user_id=g.owner_user_id)) + 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() - # Backfill membership: ensure owner is member - all_games = db.query(Game).all() - for g in all_games: - exists = db.query(GameMember).filter(GameMember.game_id == g.id, GameMember.user_id == g.owner_user_id).first() - if not exists: - db.add(GameMember(game_id=g.id, user_id=g.owner_user_id)) - db.commit() def seed_entries(db: Session): if db.query(Entry).count() > 0: return - suspects = ["Draco Malfoy","Crabbe & Goyle","Lucius Malfoy","Dolores Umbridge","Peter Pettigrew","Bellatrix Lestrange"] - items = ["Schlaftrunk","Verschwindekabinett","Portschlüssel","Impedimenta","Petrificus Totalus","Alraune"] - locations = ["Große Halle","Krankenflügel","Raum der Wünsche","Klassenzimmer für Zaubertränke","Pokalszimmer","Klassenzimmer für Wahrsagen","Eulerei","Bibliothek","Verteidigung gegen die dunklen Künste"] + suspects = ["Draco Malfoy", "Crabbe & Goyle", "Lucius Malfoy", "Dolores Umbridge", "Peter Pettigrew", "Bellatrix Lestrange"] + items = ["Schlaftrunk", "Verschwindekabinett", "Portschlüssel", "Impedimenta", "Petrificus Totalus", "Alraune"] + locations = ["Große Halle", "Krankenflügel", "Raum der Wünsche", "Klassenzimmer für Zaubertränke", "Pokalszimmer", "Klassenzimmer für Wahrsagen", "Eulerei", "Bibliothek", "Verteidigung gegen die dunklen Künste"] for s in suspects: db.add(Entry(category=Category.suspect.value, label=s)) @@ -111,6 +252,7 @@ def seed_entries(db: Session): db.add(Entry(category=Category.location.value, label=l)) db.commit() + def ensure_admin(db: Session): admin_email = os.environ.get("ADMIN_EMAIL", "admin@local").lower().strip() admin_pw = os.environ.get("ADMIN_PASSWORD", "ChangeMeNow123!") @@ -126,6 +268,7 @@ def ensure_admin(db: Session): ) db.commit() + @app.on_event("startup") def on_startup(): # create new tables