feat: expose explicit source conflicts
This commit is contained in:
@@ -80,6 +80,7 @@ def activity_rows(
|
||||
sources=coordinate_sources,
|
||||
coordinate_precision=coordinate_precision,
|
||||
coordinate_sources=coordinate_sources,
|
||||
source_conflicts=_source_conflicts(items),
|
||||
))
|
||||
return sorted(result, key=lambda row: (row.activity_score, row.last_confirmed_at), reverse=True)
|
||||
|
||||
@@ -95,6 +96,16 @@ def _source_system(report: CatchReport) -> str:
|
||||
return "manual-import"
|
||||
|
||||
|
||||
def _source_conflicts(reports: list[CatchReport]) -> list[str]:
|
||||
conflicts: set[str] = set()
|
||||
for report in reports:
|
||||
provenance = (report.raw_payload or {}).get("provenance", {})
|
||||
values = provenance.get("conflicts", []) if isinstance(provenance, dict) else []
|
||||
if isinstance(values, list):
|
||||
conflicts.update(str(value).strip() for value in values if str(value).strip())
|
||||
return sorted(conflicts)
|
||||
|
||||
|
||||
def _coordinate_precision(report: CatchReport) -> str:
|
||||
provenance = (report.raw_payload or {}).get("provenance", {})
|
||||
value = provenance.get("coordinate_precision") if isinstance(provenance, dict) else None
|
||||
|
||||
@@ -7,7 +7,7 @@ from fastapi import APIRouter, HTTPException, Query, Response
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session, joinedload, selectinload
|
||||
|
||||
from ..activity import activity_rows
|
||||
from ..activity import _source_conflicts, activity_rows
|
||||
from ..config import settings
|
||||
from ..dependencies import Db
|
||||
from ..models import CatchReport, ModerationStatus, SourceType, Spot, Waterbody
|
||||
@@ -76,7 +76,7 @@ def spot_detail(spot_id: UUID, db: Db) -> SpotOut:
|
||||
precisions = [item.get("coordinate_precision") for item in provenance]
|
||||
precision = max((value for value in precisions if value in {"exact", "approximate", "area", "missing"}), key={"exact": 0, "approximate": 1, "area": 2, "missing": 3}.get, default="exact")
|
||||
sources = sorted({str(item.get("source_system")) for item in provenance if item.get("source_system")}) or ["players"]
|
||||
return SpotOut(id=spot.id, waterbody_slug=spot.waterbody.slug, waterbody=spot.waterbody.name_ru, x=spot.x, y=spot.y, description=spot.description, catches_24h=count_since(timedelta(hours=24)), catches_3d=count_since(timedelta(days=3)), catches_7d=count_since(timedelta(days=7)), top_baits=[name for name, _ in bait_counts.most_common(5)], coordinate_precision=precision, coordinate_sources=sources)
|
||||
return SpotOut(id=spot.id, waterbody_slug=spot.waterbody.slug, waterbody=spot.waterbody.name_ru, x=spot.x, y=spot.y, description=spot.description, catches_24h=count_since(timedelta(hours=24)), catches_3d=count_since(timedelta(days=3)), catches_7d=count_since(timedelta(days=7)), top_baits=[name for name, _ in bait_counts.most_common(5)], coordinate_precision=precision, coordinate_sources=sources, source_conflicts=_source_conflicts(reports))
|
||||
|
||||
|
||||
def _report_source(report: CatchReport) -> str:
|
||||
|
||||
@@ -102,6 +102,7 @@ class ActivityOut(BaseModel):
|
||||
sources: list[str]
|
||||
coordinate_precision: str
|
||||
coordinate_sources: list[str]
|
||||
source_conflicts: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PaginatedActivityOut(BaseModel):
|
||||
@@ -160,6 +161,7 @@ class SpotOut(BaseModel):
|
||||
top_baits: list[str]
|
||||
coordinate_precision: str
|
||||
coordinate_sources: list[str]
|
||||
source_conflicts: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class OfficialRecordOut(BaseModel):
|
||||
|
||||
@@ -55,6 +55,20 @@ def test_activity_filters_and_explains_score() -> None:
|
||||
assert payload["items"][0]["unique_players"] == 3
|
||||
assert "3 свежих улова" in payload["items"][0]["explanation"]
|
||||
assert payload["items"][0]["sources"] == ["manual-import"]
|
||||
assert payload["items"][0]["source_conflicts"] == []
|
||||
|
||||
|
||||
def test_explicit_source_conflict_is_exposed_on_activity_and_spot() -> None:
|
||||
with Session(engine) as db:
|
||||
report = db.scalar(select(CatchReport).where(CatchReport.waterbody.has(slug="test-lake")))
|
||||
assert report is not None
|
||||
report.raw_payload = {"provenance": {"conflicts": ["координаты: 10:20 · 11:21"]}}
|
||||
spot_id = report.spot_id
|
||||
db.commit()
|
||||
activity = client.get("/api/v1/activity?waterbody=test-lake&fish=pike&hours=6").json()
|
||||
assert activity["items"][0]["source_conflicts"] == ["координаты: 10:20 · 11:21"]
|
||||
spot = client.get(f"/api/v1/spots/{spot_id}").json()
|
||||
assert spot["source_conflicts"] == ["координаты: 10:20 · 11:21"]
|
||||
|
||||
|
||||
def test_waterbody_catalog_exposes_nullable_source_provenance() -> None:
|
||||
|
||||
Reference in New Issue
Block a user