Files
rf4-spotter/apps/api/app/community_review.py
T

132 lines
5.2 KiB
Python

from __future__ import annotations
import hashlib
from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.orm import Session
from .importer import normalize
from .models import (
Bait, BaitKind, CatchReport, ExternalEntityAlias, ExternalObservation,
Fish, ModerationStatus, SourceType, Spot, Waterbody,
)
class ExternalReviewError(ValueError):
pass
def map_observation(
session: Session, observation: ExternalObservation, fish: Fish, waterbody: Waterbody,
*, note: str | None = None,
) -> ExternalObservation:
if observation.status == "published":
raise ExternalReviewError("published observation cannot be remapped")
observation.fish = fish
observation.waterbody = waterbody
observation.review_note = note
observation.reviewed_at = datetime.now(timezone.utc)
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, "waterbody", observation.waterbody_external_id or observation.waterbody_name, waterbody=waterbody)
session.commit()
return observation
def reject_observation(session: Session, observation: ExternalObservation, *, reason: str) -> ExternalObservation:
if observation.status == "published":
raise ExternalReviewError("published observation cannot be rejected")
observation.status = "rejected"
observation.review_note = reason
observation.reviewed_at = datetime.now(timezone.utc)
session.commit()
return observation
def publish_observation(session: Session, observation: ExternalObservation) -> CatchReport:
if observation.catch_report is not None:
return observation.catch_report
if observation.fish is None or observation.waterbody is None or not _complete(observation):
raise ExternalReviewError("fish, waterbody, coordinates and weight are required for publication")
spot = session.scalar(select(Spot).where(
Spot.waterbody_id == observation.waterbody.id, Spot.x == observation.x, Spot.y == observation.y,
))
if spot is None:
spot = Spot(waterbody=observation.waterbody, x=observation.x, y=observation.y)
session.add(spot)
bait = _bait(session, observation.payload.get("bait"))
now = datetime.now(timezone.utc)
report = CatchReport(
fish=observation.fish, waterbody=observation.waterbody, spot=spot, bait=bait,
weight_g=observation.weight_g,
fishing_method=observation.payload.get("fishing_method"),
rig_type=observation.payload.get("rig_type"),
retrieve_method=observation.payload.get("retrieve_method"),
retrieve_speed=observation.payload.get("retrieve_speed"),
caught_at=observation.published_at,
reported_at=observation.published_at or observation.first_seen_at,
player_name=observation.payload.get("player_name"),
source_type=SourceType.manual_import,
source_url=observation.source_url,
source_external_id=_report_external_id(observation),
source_confidence=observation.source.default_confidence,
moderation_status=ModerationStatus.approved,
raw_payload={
"provenance": {
"external_observation_id": str(observation.id),
"source_system": observation.source_system,
"source_external_id": observation.source_external_id,
},
"original": observation.payload,
},
)
session.add(report)
session.flush()
observation.catch_report = report
observation.status = "published"
observation.reviewed_at = now
session.commit()
return report
def _complete(observation: ExternalObservation) -> bool:
return observation.x is not None and observation.y is not None and observation.weight_g is not None
def _save_alias(
session: Session, observation: ExternalObservation, entity_type: str, external_id: str,
*, fish: Fish | None = None, waterbody: Waterbody | None = None,
) -> None:
alias = session.scalar(select(ExternalEntityAlias).where(
ExternalEntityAlias.source_system == observation.source_system,
ExternalEntityAlias.entity_type == entity_type,
ExternalEntityAlias.external_id == external_id,
))
if alias is None:
alias = ExternalEntityAlias(
source_system=observation.source_system, entity_type=entity_type,
external_id=external_id, external_name=observation.fish_name if fish else observation.waterbody_name,
)
session.add(alias)
alias.fish = fish
alias.waterbody = waterbody
alias.updated_at = datetime.now(timezone.utc)
def _bait(session: Session, value: object) -> Bait | None:
name = str(value or "").strip()
if not name:
return None
key = normalize(name)
bait = session.scalar(select(Bait).where(Bait.normalized_name == key))
if bait is None:
bait = Bait(name=name[:200], normalized_name=key[:200], kind=BaitKind.unknown)
session.add(bait)
return bait
def _report_external_id(observation: ExternalObservation) -> str:
raw = f"{observation.source_system}:{observation.source_external_id}".encode()
return "ext:" + hashlib.sha256(raw).hexdigest()[:60]