From 21c0ffbe75977864d128957fc44e2d9dedb07db4 Mon Sep 17 00:00:00 2001 From: IK Date: Mon, 21 Sep 2026 20:25:51 +0700 Subject: [PATCH] feat: add bulk external observation rejection --- apps/api/app/routers/admin.py | 16 +++++++++++++++- apps/api/app/schemas.py | 14 ++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/apps/api/app/routers/admin.py b/apps/api/app/routers/admin.py index 2461412..416ea56 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, ExternalObservationDecision, ExternalObservationMapping, ExternalObservationOut, ExternalObservationPublished, ImportRunOut, ModerationBulkResult, ModerationBulkUpdate, ModerationUpdate +from ..schemas import AdminCatchReportOut, AdminMediaDecision, AdminMediaReviewOut, AdminMediaRollback, AdminModerationHistoryOut, AdminSourceStatusOut, CatchReportCreated, ExternalAliasSuggestionOut, ExternalObservationAction, ExternalObservationBulkDecision, ExternalObservationBulkResult, ExternalObservationDecision, ExternalObservationMapping, ExternalObservationOut, ExternalObservationPublished, ImportRunOut, ModerationBulkResult, ModerationBulkUpdate, ModerationUpdate from ..storage import delete_screenshot, signed_screenshot_url from ..time_utils import aware @@ -378,6 +378,20 @@ def admin_reject_external_observation( raise HTTPException(status_code=409, detail=str(exc)) from exc +@router.patch("/api/v1/admin/external-observations/bulk-reject", response_model=ExternalObservationBulkResult) +def admin_reject_external_observations_bulk(payload: ExternalObservationBulkDecision, 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())) + by_id = {item.id: item for item in observations} + expected = {item.id: item.expected_version for item in payload.items} + if len(by_id) != len(ids) or any(item.status in {"published", "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") + for item in observations: + item.moderation_version += 1 + reject_observation(db, item, reason=payload.reason) + return ExternalObservationBulkResult(updated=len(observations)) + + @router.get("/api/v1/admin/catch-reports", response_model=list[AdminCatchReportOut]) def admin_reports(db: Db, _: Annotated[str, Depends(_admin)], status: ModerationStatus = ModerationStatus.pending, limit: int = Query(50, ge=1, le=100), offset: int = Query(0, ge=0)) -> list[AdminCatchReportOut]: reports = list(db.scalars(select(CatchReport).options(joinedload(CatchReport.fish), joinedload(CatchReport.waterbody), joinedload(CatchReport.spot), joinedload(CatchReport.bait)).where(CatchReport.source_type == SourceType.user, CatchReport.moderation_status == status, CatchReport.deleted_at.is_(None)).order_by(CatchReport.reported_at, CatchReport.id).offset(offset).limit(limit))) diff --git a/apps/api/app/schemas.py b/apps/api/app/schemas.py index 47232f2..cd3988b 100644 --- a/apps/api/app/schemas.py +++ b/apps/api/app/schemas.py @@ -403,6 +403,20 @@ class ExternalObservationDecision(BaseModel): expected_version: int = Field(ge=0) +class ExternalObservationBulkItem(BaseModel): + id: UUID + expected_version: int = Field(ge=0) + + +class ExternalObservationBulkDecision(BaseModel): + reason: str = Field(min_length=1, max_length=1000) + items: list[ExternalObservationBulkItem] = Field(min_length=1, max_length=50) + + +class ExternalObservationBulkResult(BaseModel): + updated: int + + class ExternalObservationAction(BaseModel): expected_version: int = Field(ge=0)