feat: add bulk external observation mapping

This commit is contained in:
ik
2026-09-21 20:30:41 +07:00
parent bc02d28169
commit 28a361314a
3 changed files with 39 additions and 3 deletions
+2 -1
View File
@@ -21,7 +21,7 @@ class ExternalReviewError(ValueError):
def map_observation( def map_observation(
session: Session, observation: ExternalObservation, fish: Fish, waterbody: Waterbody, session: Session, observation: ExternalObservation, fish: Fish, waterbody: Waterbody,
*, note: str | None = None, *, note: str | None = None, commit: bool = True,
) -> ExternalObservation: ) -> ExternalObservation:
if observation.status == "published": if observation.status == "published":
raise ExternalReviewError("published observation cannot be remapped") raise ExternalReviewError("published observation cannot be remapped")
@@ -43,6 +43,7 @@ def map_observation(
observation.status = "ready" if _complete(observation) else "mapped" observation.status = "ready" if _complete(observation) else "mapped"
_save_alias(session, observation, "fish", observation.fish_external_id or observation.fish_name, fish=fish) _save_alias(session, observation, "fish", observation.fish_external_id or observation.fish_name, fish=fish)
_save_alias(session, observation, "waterbody", observation.waterbody_external_id or observation.waterbody_name, waterbody=waterbody) _save_alias(session, observation, "waterbody", observation.waterbody_external_id or observation.waterbody_name, waterbody=waterbody)
if commit:
session.commit() session.commit()
return observation return observation
+29 -1
View File
@@ -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 ..media_catalog import MEDIA_ROOT, review_assets, review_file
from ..models import CatchReport, CommunityImportRun, DataSource, ExternalObservation, Fish, ModerationEvent, ModerationStatus, OfficialRecordImport, SourceType, Waterbody from ..models import CatchReport, CommunityImportRun, DataSource, ExternalObservation, Fish, ModerationEvent, ModerationStatus, OfficialRecordImport, SourceType, Waterbody
from ..public_cache import public_cache from ..public_cache import public_cache
from ..schemas import AdminCatchReportOut, AdminMediaDecision, AdminMediaReviewOut, AdminMediaRollback, AdminModerationHistoryOut, AdminSourceStatusOut, CatchReportCreated, ExternalAliasSuggestionOut, ExternalObservationAction, ExternalObservationBulkDecision, ExternalObservationBulkResult, ExternalObservationDecision, ExternalObservationMapping, ExternalObservationOut, ExternalObservationPublished, ImportRunOut, ModerationBulkResult, ModerationBulkUpdate, ModerationUpdate from ..schemas import AdminCatchReportOut, AdminMediaDecision, AdminMediaReviewOut, AdminMediaRollback, AdminModerationHistoryOut, AdminSourceStatusOut, CatchReportCreated, ExternalAliasSuggestionOut, ExternalObservationAction, ExternalObservationBulkDecision, ExternalObservationBulkMapping, ExternalObservationBulkResult, ExternalObservationDecision, ExternalObservationMapping, ExternalObservationOut, ExternalObservationPublished, ImportRunOut, ModerationBulkResult, ModerationBulkUpdate, ModerationUpdate
from ..storage import delete_screenshot, signed_screenshot_url from ..storage import delete_screenshot, signed_screenshot_url
from ..time_utils import aware from ..time_utils import aware
@@ -343,6 +343,34 @@ def admin_map_external_observation(
raise HTTPException(status_code=409, detail=str(exc)) from exc raise HTTPException(status_code=409, detail=str(exc)) from exc
@router.patch("/api/v1/admin/external-observations/bulk-mapping", response_model=ExternalObservationBulkResult)
def admin_map_external_observations_bulk(
payload: ExternalObservationBulkMapping, 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", "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")
fish = db.scalar(select(Fish).where(Fish.slug == payload.fish_slug))
waterbody = db.scalar(select(Waterbody).where(Waterbody.slug == payload.waterbody_slug))
if fish is None or waterbody is None:
raise HTTPException(status_code=422, detail="unknown fish or waterbody")
try:
for item in observations:
item.moderation_version += 1
map_observation(db, item, fish, waterbody, note=payload.note, commit=False)
db.commit()
except ExternalReviewError as exc:
db.rollback()
raise HTTPException(status_code=409, detail=str(exc)) from exc
return ExternalObservationBulkResult(updated=len(observations))
@router.post("/api/v1/admin/external-observations/{observation_id}/publish", response_model=ExternalObservationPublished) @router.post("/api/v1/admin/external-observations/{observation_id}/publish", response_model=ExternalObservationPublished)
def admin_publish_external_observation( def admin_publish_external_observation(
observation_id: UUID, payload: ExternalObservationAction, db: Db, _: Annotated[str, Depends(_admin)], observation_id: UUID, payload: ExternalObservationAction, db: Db, _: Annotated[str, Depends(_admin)],
+7
View File
@@ -413,6 +413,13 @@ class ExternalObservationBulkDecision(BaseModel):
items: list[ExternalObservationBulkItem] = Field(min_length=1, max_length=50) items: list[ExternalObservationBulkItem] = Field(min_length=1, max_length=50)
class ExternalObservationBulkMapping(BaseModel):
fish_slug: str
waterbody_slug: str
note: str | None = Field(default=None, max_length=1000)
items: list[ExternalObservationBulkItem] = Field(min_length=1, max_length=50)
class ExternalObservationBulkResult(BaseModel): class ExternalObservationBulkResult(BaseModel):
updated: int updated: int