Catch only requests.RequestException in refresh_overlay so that server-side data errors (e.g., ValueError) bubble up as 500 rather than being disguised as a 502 "steam api error". Update the 502 test to use a real requests exception, add a sibling test that verifies non-requests exceptions propagate, and explicitly assert that refresh enqueues a build_overlay job even when Steam returns no entries. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
192 lines
6.6 KiB
Python
192 lines
6.6 KiB
Python
"""Routes for the workshop overlay type (add/remove items, manual rebuild,
|
|
admin global refresh)."""
|
|
from __future__ import annotations
|
|
|
|
import requests
|
|
from flask import Blueprint, Response, redirect, request
|
|
from sqlalchemy import delete as sa_delete
|
|
from sqlalchemy import select
|
|
|
|
from l4d2web.auth import current_user, require_admin, require_login
|
|
from l4d2web.db import session_scope
|
|
from l4d2web.models import (
|
|
Job,
|
|
Overlay,
|
|
OverlayWorkshopItem,
|
|
WorkshopItem,
|
|
)
|
|
from l4d2web.services import steam_workshop
|
|
from l4d2web.services.job_worker import enqueue_build_overlay
|
|
|
|
|
|
bp = Blueprint("workshop", __name__)
|
|
|
|
|
|
def _check_workshop_overlay_access(overlay_id: int, user, db):
|
|
overlay = db.scalar(select(Overlay).where(Overlay.id == overlay_id))
|
|
if overlay is None:
|
|
return None, Response(status=404)
|
|
if overlay.type != "workshop":
|
|
return None, Response("not a workshop overlay", status=400)
|
|
if overlay.user_id != user.id and not user.admin:
|
|
return None, Response(status=403)
|
|
return overlay, None
|
|
|
|
|
|
@bp.post("/overlays/<int:overlay_id>/items")
|
|
@require_login
|
|
def add_items(overlay_id: int) -> Response:
|
|
user = current_user()
|
|
assert user is not None
|
|
|
|
raw_input = request.form.get("input", "").strip()
|
|
mode = request.form.get("input_mode", "items")
|
|
if not raw_input:
|
|
return Response("missing input", status=400)
|
|
|
|
try:
|
|
ids = steam_workshop.parse_workshop_input(raw_input)
|
|
except ValueError as exc:
|
|
return Response(str(exc), status=400)
|
|
|
|
if mode == "collection":
|
|
if len(ids) != 1:
|
|
return Response("collection mode expects exactly one id or url", status=400)
|
|
try:
|
|
ids = steam_workshop.resolve_collection(ids[0])
|
|
except Exception as exc:
|
|
return Response(f"failed to resolve collection: {exc}", status=502)
|
|
if not ids:
|
|
return Response("collection has no items", status=400)
|
|
|
|
try:
|
|
metas = steam_workshop.fetch_metadata_batch(ids, mode="add")
|
|
except steam_workshop.WorkshopValidationError as exc:
|
|
return Response(str(exc), status=400)
|
|
except Exception as exc:
|
|
return Response(f"steam api error: {exc}", status=502)
|
|
|
|
with session_scope() as db:
|
|
overlay, err = _check_workshop_overlay_access(overlay_id, user, db)
|
|
if err is not None:
|
|
return err
|
|
for meta in metas:
|
|
wi = db.scalar(select(WorkshopItem).where(WorkshopItem.steam_id == meta.steam_id))
|
|
if wi is None:
|
|
wi = WorkshopItem(steam_id=meta.steam_id)
|
|
db.add(wi)
|
|
wi.title = meta.title
|
|
wi.filename = meta.filename
|
|
wi.file_url = meta.file_url
|
|
wi.file_size = meta.file_size
|
|
wi.time_updated = meta.time_updated
|
|
wi.preview_url = meta.preview_url
|
|
wi.last_error = "" if meta.result == 1 else f"steam result {meta.result}"
|
|
db.flush()
|
|
|
|
existing = db.scalar(
|
|
select(OverlayWorkshopItem).where(
|
|
OverlayWorkshopItem.overlay_id == overlay_id,
|
|
OverlayWorkshopItem.workshop_item_id == wi.id,
|
|
)
|
|
)
|
|
if existing is None:
|
|
db.add(OverlayWorkshopItem(overlay_id=overlay_id, workshop_item_id=wi.id))
|
|
|
|
job = enqueue_build_overlay(db, overlay_id=overlay_id, user_id=user.id)
|
|
job_id = job.id
|
|
|
|
return redirect(f"/jobs/{job_id}")
|
|
|
|
|
|
@bp.post("/overlays/<int:overlay_id>/items/<int:item_id>/delete")
|
|
@require_login
|
|
def remove_item(overlay_id: int, item_id: int) -> Response:
|
|
user = current_user()
|
|
assert user is not None
|
|
with session_scope() as db:
|
|
overlay, err = _check_workshop_overlay_access(overlay_id, user, db)
|
|
if err is not None:
|
|
return err
|
|
result = db.execute(
|
|
sa_delete(OverlayWorkshopItem).where(
|
|
OverlayWorkshopItem.overlay_id == overlay_id,
|
|
OverlayWorkshopItem.workshop_item_id == item_id,
|
|
)
|
|
)
|
|
if result.rowcount == 0:
|
|
return Response(status=404)
|
|
job = enqueue_build_overlay(db, overlay_id=overlay_id, user_id=user.id)
|
|
job_id = job.id
|
|
return redirect(f"/jobs/{job_id}")
|
|
|
|
|
|
@bp.post("/overlays/<int:overlay_id>/refresh")
|
|
@require_login
|
|
def refresh_overlay(overlay_id: int) -> Response:
|
|
user = current_user()
|
|
assert user is not None
|
|
with session_scope() as db:
|
|
overlay, err = _check_workshop_overlay_access(overlay_id, user, db)
|
|
if err is not None:
|
|
return err
|
|
steam_ids = list(
|
|
db.scalars(
|
|
select(WorkshopItem.steam_id)
|
|
.join(
|
|
OverlayWorkshopItem,
|
|
OverlayWorkshopItem.workshop_item_id == WorkshopItem.id,
|
|
)
|
|
.where(OverlayWorkshopItem.overlay_id == overlay_id)
|
|
).all()
|
|
)
|
|
|
|
if not steam_ids:
|
|
return Response("overlay has no items", status=400)
|
|
|
|
try:
|
|
metas = steam_workshop.fetch_metadata_batch(steam_ids, mode="refresh")
|
|
except requests.RequestException as exc:
|
|
return Response(f"steam api error: {exc}", status=502)
|
|
|
|
metas_by_id = {m.steam_id: m for m in metas}
|
|
with session_scope() as db:
|
|
overlay, err = _check_workshop_overlay_access(overlay_id, user, db)
|
|
if err is not None:
|
|
return err
|
|
for steam_id in steam_ids:
|
|
wi = db.scalar(select(WorkshopItem).where(WorkshopItem.steam_id == steam_id))
|
|
if wi is None:
|
|
continue
|
|
meta = metas_by_id.get(steam_id)
|
|
if meta is None:
|
|
wi.last_error = "steam returned no entry for this item"
|
|
continue
|
|
wi.title = meta.title
|
|
wi.filename = meta.filename
|
|
wi.file_url = meta.file_url
|
|
wi.file_size = meta.file_size
|
|
wi.time_updated = meta.time_updated
|
|
wi.preview_url = meta.preview_url
|
|
wi.last_error = "" if meta.result == 1 else f"steam result {meta.result}"
|
|
job = enqueue_build_overlay(db, overlay_id=overlay_id, user_id=user.id)
|
|
job_id = job.id
|
|
|
|
return redirect(f"/jobs/{job_id}")
|
|
|
|
|
|
@bp.post("/admin/workshop/refresh")
|
|
@require_admin
|
|
def admin_refresh() -> Response:
|
|
user = current_user()
|
|
assert user is not None
|
|
with session_scope() as db:
|
|
db.add(
|
|
Job(
|
|
user_id=user.id,
|
|
server_id=None,
|
|
operation="refresh_workshop_items",
|
|
state="queued",
|
|
)
|
|
)
|
|
return redirect("/admin/jobs")
|