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}")
|
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")
|
@bp.post("/admin/workshop/refresh")
|
||||||
@require_admin
|
@require_admin
|
||||||
def admin_refresh() -> Response:
|
def admin_refresh() -> Response:
|
||||||
|
|
|
||||||
|
|
@ -283,3 +283,142 @@ def test_other_user_cannot_modify_workshop_overlay(overlay_for):
|
||||||
headers={"X-CSRF-Token": "test-token"},
|
headers={"X-CSRF-Token": "test-token"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 403
|
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