feat: add moderation decision history
This commit is contained in:
+30
-1
@@ -31,8 +31,9 @@ from .routers.activity import router as activity_router
|
||||
from .routers.catalog import router as catalog_router
|
||||
from .routers.public_data import router as public_data_router
|
||||
from .routers.submissions import router as submissions_router
|
||||
from .time_utils import aware
|
||||
from .public_cache import public_cache
|
||||
from .schemas import ActivityOut, AdminCatchReportOut, CatchReportAccepted, CatchReportCreate, CatchReportCreated, ExternalAliasSuggestionOut, ExternalObservationDecision, ExternalObservationMapping, ExternalObservationOut, ExternalObservationPublished, ImportRunOut, ModerationUpdate
|
||||
from .schemas import ActivityOut, AdminCatchReportOut, AdminModerationHistoryOut, CatchReportAccepted, CatchReportCreate, CatchReportCreated, ExternalAliasSuggestionOut, ExternalObservationDecision, ExternalObservationMapping, ExternalObservationOut, ExternalObservationPublished, ImportRunOut, ModerationUpdate
|
||||
from .storage import ScreenshotError, client as storage_client, delete_screenshot, signed_screenshot_url, upload_screenshot
|
||||
from .submission_security import check_rate_limit
|
||||
from .submission_security import is_trusted_proxy as _is_trusted_proxy
|
||||
@@ -133,6 +134,34 @@ def admin_diagnostics(db: Db, _: Annotated[str, Depends(_admin)]) -> JSONRespons
|
||||
return JSONResponse(payload, headers={"Content-Disposition": "attachment; filename=rf4spotter-diagnostics.json"})
|
||||
|
||||
|
||||
@app.get("/api/v1/admin/moderation-history", response_model=list[AdminModerationHistoryOut])
|
||||
def admin_moderation_history(
|
||||
db: Db,
|
||||
_: Annotated[str, Depends(_admin)],
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
) -> list[AdminModerationHistoryOut]:
|
||||
report_events = list(db.scalars(
|
||||
select(ModerationEvent).order_by(ModerationEvent.created_at.desc()).limit(limit + offset)
|
||||
))
|
||||
external_events = list(db.scalars(
|
||||
select(ExternalObservation).where(ExternalObservation.reviewed_at.is_not(None))
|
||||
.order_by(ExternalObservation.reviewed_at.desc()).limit(limit + offset)
|
||||
))
|
||||
history = [AdminModerationHistoryOut(
|
||||
entity_type="catch_report", entity_id=event.catch_report_id,
|
||||
decided_at=event.created_at, action=event.new_status.value,
|
||||
moderator=event.moderator, reason=event.reason,
|
||||
) for event in report_events]
|
||||
history.extend(AdminModerationHistoryOut(
|
||||
entity_type="external_observation", entity_id=observation.id,
|
||||
decided_at=observation.reviewed_at, action=observation.status,
|
||||
moderator=None, reason=observation.review_note,
|
||||
) for observation in external_events if observation.reviewed_at is not None)
|
||||
history.sort(key=lambda event: aware(event.decided_at), reverse=True)
|
||||
return history[offset:offset + limit]
|
||||
|
||||
|
||||
@app.get("/api/v1/admin/imports", response_model=list[ImportRunOut])
|
||||
def admin_imports(
|
||||
db: Db,
|
||||
|
||||
@@ -247,6 +247,15 @@ class ExternalObservationMapping(BaseModel):
|
||||
class ExternalAliasSuggestionOut(BaseModel):
|
||||
fish_slug: str | None
|
||||
waterbody_slug: str | None
|
||||
|
||||
|
||||
class AdminModerationHistoryOut(BaseModel):
|
||||
entity_type: str
|
||||
entity_id: UUID
|
||||
decided_at: datetime
|
||||
action: str
|
||||
moderator: str | None
|
||||
reason: str | None
|
||||
requires_confirmation: bool = True
|
||||
|
||||
|
||||
|
||||
+132
-5
@@ -199,6 +199,65 @@
|
||||
"title": "AdminCatchReportOut",
|
||||
"type": "object"
|
||||
},
|
||||
"AdminModerationHistoryOut": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"title": "Action",
|
||||
"type": "string"
|
||||
},
|
||||
"decided_at": {
|
||||
"format": "date-time",
|
||||
"title": "Decided At",
|
||||
"type": "string"
|
||||
},
|
||||
"entity_id": {
|
||||
"format": "uuid",
|
||||
"title": "Entity Id",
|
||||
"type": "string"
|
||||
},
|
||||
"entity_type": {
|
||||
"title": "Entity Type",
|
||||
"type": "string"
|
||||
},
|
||||
"moderator": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Moderator"
|
||||
},
|
||||
"reason": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Reason"
|
||||
},
|
||||
"requires_confirmation": {
|
||||
"default": true,
|
||||
"title": "Requires Confirmation",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entity_type",
|
||||
"entity_id",
|
||||
"decided_at",
|
||||
"action",
|
||||
"moderator",
|
||||
"reason"
|
||||
],
|
||||
"title": "AdminModerationHistoryOut",
|
||||
"type": "object"
|
||||
},
|
||||
"BaitOut": {
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -565,11 +624,6 @@
|
||||
],
|
||||
"title": "Fish Slug"
|
||||
},
|
||||
"requires_confirmation": {
|
||||
"default": true,
|
||||
"title": "Requires Confirmation",
|
||||
"type": "boolean"
|
||||
},
|
||||
"waterbody_slug": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -2509,6 +2563,79 @@
|
||||
"summary": "Admin Start Official Import"
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/moderation-history": {
|
||||
"get": {
|
||||
"operationId": "admin_moderation_history_api_v1_admin_moderation_history_get",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "limit",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"default": 50,
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"title": "Limit",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "offset",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"default": 0,
|
||||
"minimum": 0,
|
||||
"title": "Offset",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "header",
|
||||
"name": "authorization",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AdminModerationHistoryOut"
|
||||
},
|
||||
"title": "Response Admin Moderation History Api V1 Admin Moderation History Get",
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Successful Response"
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Validation Error"
|
||||
}
|
||||
},
|
||||
"summary": "Admin Moderation History"
|
||||
}
|
||||
},
|
||||
"/api/v1/baits": {
|
||||
"get": {
|
||||
"operationId": "baits_api_v1_baits_get",
|
||||
|
||||
@@ -158,6 +158,11 @@ def test_admin_diagnostics_exposes_build_identity_only_to_admin() -> None:
|
||||
serialized = response.text.lower()
|
||||
for forbidden in ("player_name", "source_url", "error_summary", "raw_payload", "admin_token", "s3_"):
|
||||
assert forbidden not in serialized
|
||||
assert client.get("/api/v1/admin/moderation-history").status_code == 401
|
||||
history = client.get("/api/v1/admin/moderation-history", headers={"Authorization": "Bearer change-me-in-production"})
|
||||
assert history.status_code == 200
|
||||
for forbidden in ("player_name", "source_url", "raw_payload", "screenshot"):
|
||||
assert forbidden not in history.text.lower()
|
||||
|
||||
|
||||
def test_spot_detail_and_catches() -> None:
|
||||
|
||||
Reference in New Issue
Block a user