diff --git a/apps/api/app/community_review.py b/apps/api/app/community_review.py index ab19691..b7e7f30 100644 --- a/apps/api/app/community_review.py +++ b/apps/api/app/community_review.py @@ -75,7 +75,9 @@ def reject_observation( return observation -def publish_observation(session: Session, observation: ExternalObservation) -> CatchReport: +def publish_observation( + session: Session, observation: ExternalObservation, *, commit: bool = True, +) -> CatchReport: if observation.catch_report is not None and observation.status == "published": return observation.catch_report if observation.status == "rejected": @@ -138,7 +140,8 @@ def publish_observation(session: Session, observation: ExternalObservation) -> C observation.catch_report = report observation.status = "published" observation.reviewed_at = now - session.commit() + if commit: + session.commit() return report diff --git a/apps/api/app/routers/admin.py b/apps/api/app/routers/admin.py index f8b00cf..734893c 100644 --- a/apps/api/app/routers/admin.py +++ b/apps/api/app/routers/admin.py @@ -20,7 +20,7 @@ from rf4_research.media_assets import publish_quality_upgrades, rollback_quality 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, AdminMediaDecision, AdminMediaReviewOut, AdminMediaRollback, AdminModerationHistoryOut, AdminSourceStatusOut, CatchReportCreated, ExternalAliasSuggestionOut, ExternalObservationAction, ExternalObservationBulkDecision, ExternalObservationBulkMapping, ExternalObservationBulkResult, ExternalObservationDecision, ExternalObservationMapping, ExternalObservationOut, ExternalObservationPublished, ImportRunOut, ModerationBulkResult, ModerationBulkUpdate, ModerationUpdate +from ..schemas import AdminCatchReportOut, AdminMediaDecision, AdminMediaReviewOut, AdminMediaRollback, AdminModerationHistoryOut, AdminSourceStatusOut, CatchReportCreated, ExternalAliasSuggestionOut, ExternalObservationAction, ExternalObservationBulkAction, ExternalObservationBulkDecision, ExternalObservationBulkMapping, ExternalObservationBulkResult, ExternalObservationDecision, ExternalObservationMapping, ExternalObservationOut, ExternalObservationPublished, ImportRunOut, ModerationBulkResult, ModerationBulkUpdate, ModerationUpdate from ..storage import delete_screenshot, signed_screenshot_url from ..time_utils import aware @@ -389,6 +389,30 @@ def admin_publish_external_observation( return ExternalObservationPublished(observation_id=observation.id, catch_report_id=report.id, status=observation.status) +@router.post("/api/v1/admin/external-observations/bulk-publish", response_model=ExternalObservationBulkResult) +def admin_publish_external_observations_bulk( + payload: ExternalObservationBulkAction, db: Db, _: Annotated[str, Depends(_admin)], +) -> ExternalObservationBulkResult: + ids = [item.id for item in payload.items] + observations = list(db.scalars(select(ExternalObservation).where(ExternalObservation.id.in_(ids)).with_for_update())) + expected = {item.id: item.expected_version for item in payload.items} + if len({item.id for item in observations}) != len(ids) or any( + item.status in {"rejected", "withdrawn"} or item.moderation_version != expected[item.id] + for item in observations + ): + raise HTTPException(status_code=409, detail="one or more observations changed; reload the queue") + try: + for item in observations: + item.moderation_version += 1 + publish_observation(db, item, commit=False) + db.commit() + except ExternalReviewError as exc: + db.rollback() + raise HTTPException(status_code=409, detail=str(exc)) from exc + public_cache.invalidate() + return ExternalObservationBulkResult(updated=len(observations)) + + @router.patch("/api/v1/admin/external-observations/{observation_id}/reject", response_model=ExternalObservationOut) def admin_reject_external_observation( observation_id: UUID, payload: ExternalObservationDecision, db: Db, diff --git a/apps/api/app/schemas.py b/apps/api/app/schemas.py index 99d38b0..9f750d2 100644 --- a/apps/api/app/schemas.py +++ b/apps/api/app/schemas.py @@ -413,6 +413,10 @@ class ExternalObservationBulkDecision(BaseModel): items: list[ExternalObservationBulkItem] = Field(min_length=1, max_length=50) +class ExternalObservationBulkAction(BaseModel): + items: list[ExternalObservationBulkItem] = Field(min_length=1, max_length=50) + + class ExternalObservationBulkMapping(BaseModel): fish_slug: str waterbody_slug: str