workshop_routes: add per-overlay refresh endpoint
POST /overlays/{id}/refresh lets the overlay owner (or any admin)
re-fetch fresh Steam metadata for all items and enqueue a rebuild.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
81c6863cca
commit
f5094c2d9d
2 changed files with 193 additions and 0 deletions
|
|
@ -120,6 +120,60 @@ def remove_item(overlay_id: int, item_id: int) -> Response:
|
|||
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 Exception 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:
|
||||
|
|
|
|||
|
|
@ -283,3 +283,142 @@ def test_other_user_cannot_modify_workshop_overlay(overlay_for):
|
|||
headers={"X-CSRF-Token": "test-token"},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_overlay_refresh_owner_can_refresh_and_enqueues_build(overlay_for):
|
||||
app, login, user_id, _admin_id, overlay_id = overlay_for
|
||||
user_client = login(user_id)
|
||||
with _patch_steam([_meta("1001")]):
|
||||
user_client.post(
|
||||
f"/overlays/{overlay_id}/items",
|
||||
data={"input": "1001", "input_mode": "items"},
|
||||
headers={"X-CSRF-Token": "test-token"},
|
||||
)
|
||||
with session_scope() as session:
|
||||
for job in session.query(Job).filter_by(operation="build_overlay", state="queued"):
|
||||
job.state = "succeeded"
|
||||
|
||||
fresh_meta = steam_workshop.WorkshopMetadata(
|
||||
steam_id="1001", title="Item 1001 (updated)", filename="1001.vpk",
|
||||
file_url="https://example.com/1001.vpk", file_size=99, time_updated=1800000000,
|
||||
preview_url="https://example.com/preview-1001.jpg", consumer_app_id=550, result=1,
|
||||
)
|
||||
with patch.object(steam_workshop, "fetch_metadata_batch", return_value=[fresh_meta]) as fetch:
|
||||
response = user_client.post(
|
||||
f"/overlays/{overlay_id}/refresh",
|
||||
headers={"X-CSRF-Token": "test-token"},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
assert response.headers["Location"].startswith("/jobs/")
|
||||
fetch.assert_called_once_with(["1001"], mode="refresh")
|
||||
|
||||
with session_scope() as session:
|
||||
wi = session.query(WorkshopItem).filter_by(steam_id="1001").one()
|
||||
assert wi.title == "Item 1001 (updated)"
|
||||
assert wi.time_updated == 1800000000
|
||||
new_jobs = session.query(Job).filter_by(
|
||||
operation="build_overlay", overlay_id=overlay_id, state="queued"
|
||||
).all()
|
||||
assert len(new_jobs) == 1
|
||||
|
||||
|
||||
def test_overlay_refresh_returns_400_when_overlay_empty(overlay_for):
|
||||
app, login, user_id, _admin_id, overlay_id = overlay_for
|
||||
user_client = login(user_id)
|
||||
|
||||
with patch.object(steam_workshop, "fetch_metadata_batch") as fetch:
|
||||
response = user_client.post(
|
||||
f"/overlays/{overlay_id}/refresh",
|
||||
headers={"X-CSRF-Token": "test-token"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
fetch.assert_not_called()
|
||||
|
||||
|
||||
def test_overlay_refresh_forbidden_for_non_owner(overlay_for, env_user):
|
||||
app, login, _user_id, _admin_id, overlay_id = overlay_for
|
||||
with session_scope() as session:
|
||||
bob = User(username="bob", password_digest=hash_password("x"), admin=False)
|
||||
session.add(bob)
|
||||
session.flush()
|
||||
bob_id = bob.id
|
||||
bob_client = login(bob_id)
|
||||
response = bob_client.post(
|
||||
f"/overlays/{overlay_id}/refresh",
|
||||
headers={"X-CSRF-Token": "test-token"},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_overlay_refresh_admin_can_refresh_anyone(overlay_for):
|
||||
app, login, user_id, admin_id, overlay_id = overlay_for
|
||||
user_client = login(user_id)
|
||||
with _patch_steam([_meta("1001")]):
|
||||
user_client.post(
|
||||
f"/overlays/{overlay_id}/items",
|
||||
data={"input": "1001", "input_mode": "items"},
|
||||
headers={"X-CSRF-Token": "test-token"},
|
||||
)
|
||||
|
||||
admin_client = login(admin_id)
|
||||
with _patch_steam([_meta("1001")]):
|
||||
response = admin_client.post(
|
||||
f"/overlays/{overlay_id}/refresh",
|
||||
headers={"X-CSRF-Token": "test-token"},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
|
||||
|
||||
def test_overlay_refresh_502_on_steam_error(overlay_for):
|
||||
app, login, user_id, _admin_id, overlay_id = overlay_for
|
||||
user_client = login(user_id)
|
||||
with _patch_steam([_meta("1001")]):
|
||||
user_client.post(
|
||||
f"/overlays/{overlay_id}/items",
|
||||
data={"input": "1001", "input_mode": "items"},
|
||||
headers={"X-CSRF-Token": "test-token"},
|
||||
)
|
||||
with session_scope() as session:
|
||||
for job in session.query(Job).filter_by(operation="build_overlay", state="queued"):
|
||||
job.state = "succeeded"
|
||||
baseline_job_count = session.query(Job).filter_by(
|
||||
operation="build_overlay", overlay_id=overlay_id, state="queued"
|
||||
).count()
|
||||
|
||||
with patch.object(steam_workshop, "fetch_metadata_batch", side_effect=Exception("boom")):
|
||||
response = user_client.post(
|
||||
f"/overlays/{overlay_id}/refresh",
|
||||
headers={"X-CSRF-Token": "test-token"},
|
||||
)
|
||||
assert response.status_code == 502
|
||||
assert b"steam api error" in response.data
|
||||
|
||||
with session_scope() as session:
|
||||
n = session.query(Job).filter_by(
|
||||
operation="build_overlay", overlay_id=overlay_id, state="queued"
|
||||
).count()
|
||||
assert n == baseline_job_count
|
||||
|
||||
|
||||
def test_overlay_refresh_missing_item_records_last_error(overlay_for):
|
||||
app, login, user_id, _admin_id, overlay_id = overlay_for
|
||||
user_client = login(user_id)
|
||||
with _patch_steam([_meta("1001")]):
|
||||
user_client.post(
|
||||
f"/overlays/{overlay_id}/items",
|
||||
data={"input": "1001", "input_mode": "items"},
|
||||
headers={"X-CSRF-Token": "test-token"},
|
||||
)
|
||||
with session_scope() as session:
|
||||
for job in session.query(Job).filter_by(operation="build_overlay", state="queued"):
|
||||
job.state = "succeeded"
|
||||
|
||||
with patch.object(steam_workshop, "fetch_metadata_batch", return_value=[]):
|
||||
response = user_client.post(
|
||||
f"/overlays/{overlay_id}/refresh",
|
||||
headers={"X-CSRF-Token": "test-token"},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
with session_scope() as session:
|
||||
wi = session.query(WorkshopItem).filter_by(steam_id="1001").one()
|
||||
assert "no entry" in wi.last_error
|
||||
|
|
|
|||
Loading…
Reference in a new issue