feat: add admin media upgrade decisions
This commit is contained in:
@@ -81,9 +81,11 @@ def review_assets(entity_type: str | None = None, status: str | None = None) ->
|
||||
"height": item.get("height"),
|
||||
"content_type": item.get("content_type"),
|
||||
"image_url": f"/api/v1/admin/media/assets/{digest}",
|
||||
"asset_url": item.get("asset_url", ""),
|
||||
"source_system": source,
|
||||
"source_url": source_page,
|
||||
"duplicate_of": item.get("duplicate_of"),
|
||||
"supersedes": item.get("supersedes"),
|
||||
"derivatives": [{
|
||||
"role": variant.get("role"), "format": variant.get("format"),
|
||||
"width": variant.get("width"), "height": variant.get("height"),
|
||||
|
||||
@@ -15,10 +15,12 @@ from ..community_review import ExternalReviewError, map_observation, publish_obs
|
||||
from ..config import settings
|
||||
from ..dependencies import Db
|
||||
from ..importer import ImportAlreadyRunning, ImportSourceError, import_records
|
||||
from ..media_catalog import review_assets, review_file
|
||||
from rf4_research.media_assets import publish_quality_upgrades, rollback_quality_upgrade
|
||||
|
||||
from ..media_catalog import MEDIA_ROOT, review_assets, review_file
|
||||
from ..models import CatchReport, CommunityImportRun, DataSource, ExternalObservation, Fish, ModerationEvent, ModerationStatus, OfficialRecordImport, SourceType, Waterbody
|
||||
from ..public_cache import public_cache
|
||||
from ..schemas import AdminCatchReportOut, AdminMediaReviewOut, AdminModerationHistoryOut, AdminSourceStatusOut, CatchReportCreated, ExternalAliasSuggestionOut, ExternalObservationAction, ExternalObservationDecision, ExternalObservationMapping, ExternalObservationOut, ExternalObservationPublished, ImportRunOut, ModerationUpdate
|
||||
from ..schemas import AdminCatchReportOut, AdminMediaDecision, AdminMediaReviewOut, AdminMediaRollback, AdminModerationHistoryOut, AdminSourceStatusOut, CatchReportCreated, ExternalAliasSuggestionOut, ExternalObservationAction, ExternalObservationDecision, ExternalObservationMapping, ExternalObservationOut, ExternalObservationPublished, ImportRunOut, ModerationUpdate
|
||||
from ..storage import delete_screenshot, signed_screenshot_url
|
||||
from ..time_utils import aware
|
||||
|
||||
@@ -50,6 +52,32 @@ def admin_media_asset(digest: str, _: Annotated[str, Depends(_admin)]) -> FileRe
|
||||
return FileResponse(path, media_type=media_type, headers={"Cache-Control": "private, no-store"})
|
||||
|
||||
|
||||
@router.post("/api/v1/admin/media/upgrades/publish")
|
||||
def admin_publish_media_upgrades(
|
||||
payload: AdminMediaDecision,
|
||||
_: Annotated[str, Depends(_admin)],
|
||||
) -> dict[str, int]:
|
||||
"""Atomically publish all stored quality upgrades after an explicit decision."""
|
||||
try:
|
||||
return publish_quality_upgrades(MEDIA_ROOT / "manifest.json", note=payload.note)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/api/v1/admin/media/upgrades/rollback")
|
||||
def admin_rollback_media_upgrade(
|
||||
payload: AdminMediaRollback,
|
||||
_: Annotated[str, Depends(_admin)],
|
||||
) -> dict[str, str]:
|
||||
"""Restore one superseded fallback while retaining the reviewed candidate."""
|
||||
try:
|
||||
return rollback_quality_upgrade(
|
||||
MEDIA_ROOT / "manifest.json", asset_url=payload.asset_url, note=payload.note,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/api/v1/admin/diagnostics")
|
||||
def admin_diagnostics(db: Db, _: Annotated[str, Depends(_admin)]) -> JSONResponse:
|
||||
report_counts = {status.value: count for status, count in db.execute(
|
||||
|
||||
@@ -318,7 +318,17 @@ class AdminMediaReviewOut(BaseModel):
|
||||
height: int | None
|
||||
content_type: str | None
|
||||
image_url: str
|
||||
asset_url: str
|
||||
source_system: str
|
||||
source_url: str
|
||||
duplicate_of: str | None
|
||||
supersedes: str | None
|
||||
derivatives: list[AdminMediaDerivativeOut]
|
||||
|
||||
|
||||
class AdminMediaDecision(BaseModel):
|
||||
note: str = Field(min_length=1, max_length=1000)
|
||||
|
||||
|
||||
class AdminMediaRollback(AdminMediaDecision):
|
||||
asset_url: str = Field(min_length=1, max_length=2000)
|
||||
|
||||
@@ -13,6 +13,7 @@ from app.community_importer import stage_observations
|
||||
from app.importer import ImportAlreadyRunning
|
||||
from app.main import app
|
||||
from app.models import Bait, BaitKind, CatchReport, DataSource, ExternalEntityAlias, ExternalObservation, Fish, ImportStatus, ModerationEvent, ModerationStatus, OfficialRecordImport, SourceType, Spot, SubmissionAttempt, Waterbody
|
||||
from app.routers import admin as admin_router
|
||||
|
||||
|
||||
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
||||
@@ -182,6 +183,34 @@ def test_admin_media_review_requires_auth() -> None:
|
||||
assert all({"role", "format", "width", "height"} <= set(derivative) for derivative in response.json()[0]["derivatives"])
|
||||
|
||||
|
||||
def test_admin_media_decisions_require_auth_and_note(monkeypatch) -> None:
|
||||
assert client.post("/api/v1/admin/media/upgrades/publish", json={"note": "publish"}).status_code == 401
|
||||
assert client.post("/api/v1/admin/media/upgrades/rollback", json={"asset_url": "https://example.test/a", "note": "rollback"}).status_code == 401
|
||||
|
||||
monkeypatch.setattr(admin_router, "publish_quality_upgrades", lambda path, note: {"published": 2, "retained_fallbacks": 2})
|
||||
publish = client.post(
|
||||
"/api/v1/admin/media/upgrades/publish",
|
||||
json={"note": "visual review complete"},
|
||||
headers={"Authorization": "Bearer change-me-in-production"},
|
||||
)
|
||||
assert publish.status_code == 200
|
||||
assert publish.json() == {"published": 2, "retained_fallbacks": 2}
|
||||
|
||||
monkeypatch.setattr(admin_router, "rollback_quality_upgrade", lambda path, asset_url, note: {"rolled_back": asset_url, "restored": "https://example.test/fallback"})
|
||||
rollback = client.post(
|
||||
"/api/v1/admin/media/upgrades/rollback",
|
||||
json={"asset_url": "https://example.test/a", "note": "fallback is preferred"},
|
||||
headers={"Authorization": "Bearer change-me-in-production"},
|
||||
)
|
||||
assert rollback.status_code == 200
|
||||
assert rollback.json()["rolled_back"] == "https://example.test/a"
|
||||
assert client.post(
|
||||
"/api/v1/admin/media/upgrades/publish",
|
||||
json={"note": ""},
|
||||
headers={"Authorization": "Bearer change-me-in-production"},
|
||||
).status_code == 422
|
||||
|
||||
|
||||
def test_liveness_does_not_probe_dependencies() -> None:
|
||||
response = client.get("/health?token=must-not-be-logged")
|
||||
assert response.json() == {"status": "ok"}
|
||||
|
||||
Reference in New Issue
Block a user