dev #4
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user