feat: expose safe moderation provenance

This commit is contained in:
ik
2026-09-12 21:49:45 +07:00
parent 699a7c3857
commit def5c39aa1
6 changed files with 78 additions and 3 deletions
+15 -1
View File
@@ -162,15 +162,29 @@ def admin_start_official_import(db: Db, _: Annotated[str, Depends(_admin)]) -> O
def _external_out(item: ExternalObservation) -> ExternalObservationOut:
allowed_payload = {
key: value for key, value in (item.payload or {}).items()
if key in {
"bait", "fishing_method", "rig_type", "retrieve_method", "retrieve_speed",
"player_name", "published_at", "region", "category",
} and (value is None or isinstance(value, (str, int, float, bool)))
}
missing_fields = []
if item.x is None or item.y is None:
missing_fields.append("coordinates")
if item.weight_g is None:
missing_fields.append("weight_g")
return ExternalObservationOut(
id=item.id, source_system=item.source_system, source_external_id=item.source_external_id,
source_url=item.source_url, fish_name=item.fish_name, fish_external_id=item.fish_external_id,
waterbody_name=item.waterbody_name, waterbody_external_id=item.waterbody_external_id,
x=item.x, y=item.y, weight_g=item.weight_g, published_at=item.published_at,
last_seen_at=item.last_seen_at, status=item.status,
first_seen_at=item.first_seen_at, last_seen_at=item.last_seen_at, reviewed_at=item.reviewed_at,
status=item.status,
fish_slug=item.fish.slug if item.fish else None,
waterbody_slug=item.waterbody.slug if item.waterbody else None,
catch_report_id=item.catch_report_id, review_note=item.review_note,
missing_fields=missing_fields, source_payload=allowed_payload,
)
+4
View File
@@ -226,12 +226,16 @@ class ExternalObservationOut(BaseModel):
y: int | None
weight_g: int | None
published_at: datetime | None
first_seen_at: datetime
last_seen_at: datetime
reviewed_at: datetime | None
status: str
fish_slug: str | None
waterbody_slug: str | None
catch_report_id: UUID | None
review_note: str | None
missing_fields: list[str]
source_payload: dict[str, str | int | float | bool | None]
class ExternalObservationMapping(BaseModel):
+52 -1
View File
@@ -648,6 +648,11 @@
],
"title": "Catch Report Id"
},
"first_seen_at": {
"format": "date-time",
"title": "First Seen At",
"type": "string"
},
"fish_external_id": {
"anyOf": [
{
@@ -684,6 +689,13 @@
"title": "Last Seen At",
"type": "string"
},
"missing_fields": {
"items": {
"type": "string"
},
"title": "Missing Fields",
"type": "array"
},
"published_at": {
"anyOf": [
{
@@ -707,10 +719,45 @@
],
"title": "Review Note"
},
"reviewed_at": {
"anyOf": [
{
"format": "date-time",
"type": "string"
},
{
"type": "null"
}
],
"title": "Reviewed At"
},
"source_external_id": {
"title": "Source External Id",
"type": "string"
},
"source_payload": {
"additionalProperties": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
},
{
"type": "number"
},
{
"type": "boolean"
},
{
"type": "null"
}
]
},
"title": "Source Payload",
"type": "object"
},
"source_system": {
"title": "Source System",
"type": "string"
@@ -796,12 +843,16 @@
"y",
"weight_g",
"published_at",
"first_seen_at",
"last_seen_at",
"reviewed_at",
"status",
"fish_slug",
"waterbody_slug",
"catch_report_id",
"review_note"
"review_note",
"missing_fields",
"source_payload"
],
"title": "ExternalObservationOut",
"type": "object"
+4
View File
@@ -342,6 +342,10 @@ def test_incomplete_external_observation_is_publicly_labelled_but_not_counted()
headers = {"Authorization": "Bearer change-me-in-production"}
incomplete = client.get("/api/v1/admin/external-observations?status=review&completeness=incomplete&q=Pike", headers=headers)
assert any(item["id"] == str(observation_id) for item in incomplete.json())
provenance = next(item for item in incomplete.json() if item["id"] == str(observation_id))
assert provenance["missing_fields"] == ["weight_g"]
assert provenance["first_seen_at"] and provenance["last_seen_at"]
assert set(provenance["source_payload"]) <= {"bait", "fishing_method", "rig_type", "retrieve_method", "retrieve_speed", "player_name", "published_at", "region", "category"}
complete = client.get("/api/v1/admin/external-observations?status=review&completeness=complete&q=Pike", headers=headers)
assert all(item["id"] != str(observation_id) for item in complete.json())
prioritized = client.get("/api/v1/admin/external-observations?status=review&order=risk", headers=headers)