- Add ImportRecordEvent model to track per-record import changes - Log created/updated events for each official record import - Add alembic migration 0014 for import_record_event table - Enables audit trail for which import run modified which records
227 lines
10 KiB
Python
227 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import enum
|
|
import uuid
|
|
from datetime import datetime, time
|
|
|
|
from sqlalchemy import JSON, CheckConstraint, DateTime, Enum, ForeignKey, Integer, String, Text, Time, UniqueConstraint
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from .database import Base
|
|
|
|
|
|
class SourceType(str, enum.Enum):
|
|
official_record = "official_record"
|
|
user = "user"
|
|
manual_import = "manual_import"
|
|
|
|
|
|
class ModerationStatus(str, enum.Enum):
|
|
pending = "pending"
|
|
approved = "approved"
|
|
rejected = "rejected"
|
|
|
|
|
|
class BaitKind(str, enum.Enum):
|
|
bait = "bait"
|
|
lure = "lure"
|
|
unknown = "unknown"
|
|
|
|
|
|
class ImportStatus(str, enum.Enum):
|
|
running = "running"
|
|
success = "success"
|
|
partial = "partial"
|
|
failed = "failed"
|
|
|
|
|
|
class Fish(Base):
|
|
__tablename__ = "fish"
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
slug: Mapped[str] = mapped_column(String(100), unique=True)
|
|
name_ru: Mapped[str] = mapped_column(String(200), unique=True)
|
|
trophy_weight_g: Mapped[int | None]
|
|
|
|
|
|
class Waterbody(Base):
|
|
__tablename__ = "waterbody"
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
slug: Mapped[str] = mapped_column(String(100), unique=True)
|
|
name_ru: Mapped[str] = mapped_column(String(200), unique=True)
|
|
unlock_level: Mapped[int | None]
|
|
|
|
|
|
class Bait(Base):
|
|
__tablename__ = "bait"
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
name: Mapped[str] = mapped_column(String(200))
|
|
normalized_name: Mapped[str] = mapped_column(String(200), unique=True)
|
|
kind: Mapped[BaitKind] = mapped_column(Enum(BaitKind))
|
|
|
|
|
|
class Spot(Base):
|
|
__tablename__ = "spot"
|
|
__table_args__ = (UniqueConstraint("waterbody_id", "x", "y"),)
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
waterbody_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("waterbody.id"))
|
|
x: Mapped[int]
|
|
y: Mapped[int]
|
|
description: Mapped[str | None] = mapped_column(Text)
|
|
waterbody: Mapped[Waterbody] = relationship()
|
|
|
|
|
|
class CatchReport(Base):
|
|
__tablename__ = "catch_report"
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
fish_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("fish.id"))
|
|
spot_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("spot.id"))
|
|
waterbody_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("waterbody.id"))
|
|
bait_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("bait.id"))
|
|
weight_g: Mapped[int]
|
|
fishing_method: Mapped[str | None] = mapped_column(String(50))
|
|
rig_type: Mapped[str | None] = mapped_column(String(100))
|
|
retrieve_method: Mapped[str | None] = mapped_column(String(100))
|
|
retrieve_speed: Mapped[int | None]
|
|
game_time: Mapped[time | None] = mapped_column(Time)
|
|
caught_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
reported_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
player_name: Mapped[str | None] = mapped_column(String(100))
|
|
source_type: Mapped[SourceType] = mapped_column(Enum(SourceType))
|
|
source_url: Mapped[str | None] = mapped_column(Text)
|
|
source_external_id: Mapped[str | None] = mapped_column(String(64), unique=True)
|
|
source_confidence: Mapped[int]
|
|
moderation_status: Mapped[ModerationStatus] = mapped_column(Enum(ModerationStatus))
|
|
screenshot_key: Mapped[str | None] = mapped_column(Text)
|
|
screenshot_upload_token_hash: Mapped[str | None] = mapped_column(String(64))
|
|
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
raw_payload: Mapped[dict | None] = mapped_column(JSON)
|
|
fish: Mapped[Fish] = relationship()
|
|
spot: Mapped[Spot | None] = relationship()
|
|
waterbody: Mapped[Waterbody] = relationship()
|
|
bait: Mapped[Bait | None] = relationship()
|
|
|
|
|
|
class OfficialRecordImport(Base):
|
|
__tablename__ = "official_record_import"
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
status: Mapped[ImportStatus] = mapped_column(Enum(ImportStatus))
|
|
source_url: Mapped[str] = mapped_column(Text)
|
|
rows_seen: Mapped[int] = mapped_column(default=0)
|
|
rows_created: Mapped[int] = mapped_column(default=0)
|
|
rows_updated: Mapped[int] = mapped_column(default=0)
|
|
error_summary: Mapped[str | None] = mapped_column(Text)
|
|
response_status: Mapped[int | None]
|
|
response_etag: Mapped[str | None] = mapped_column(String(500))
|
|
response_last_modified: Mapped[str | None] = mapped_column(String(500))
|
|
response_content_type: Mapped[str | None] = mapped_column(String(200))
|
|
response_bytes: Mapped[int | None]
|
|
not_modified: Mapped[bool] = mapped_column(default=False)
|
|
|
|
|
|
class ModerationEvent(Base):
|
|
__tablename__ = "moderation_event"
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
catch_report_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("catch_report.id"))
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
previous_status: Mapped[ModerationStatus] = mapped_column(Enum(ModerationStatus, name="moderationstatus", create_type=False))
|
|
new_status: Mapped[ModerationStatus] = mapped_column(Enum(ModerationStatus, name="moderationstatus", create_type=False))
|
|
moderator: Mapped[str] = mapped_column(String(100))
|
|
reason: Mapped[str | None] = mapped_column(Text)
|
|
catch_report: Mapped[CatchReport] = relationship()
|
|
|
|
|
|
class SubmissionAttempt(Base):
|
|
__tablename__ = "submission_attempt"
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
client_hash: Mapped[str] = mapped_column(String(64), index=True)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)
|
|
|
|
|
|
class DataSource(Base):
|
|
__tablename__ = "data_source"
|
|
key: Mapped[str] = mapped_column(String(50), primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(100))
|
|
base_url: Mapped[str] = mapped_column(Text)
|
|
default_confidence: Mapped[int]
|
|
enabled: Mapped[bool] = mapped_column(default=True)
|
|
|
|
|
|
class CommunityImportRun(Base):
|
|
__tablename__ = "community_import_run"
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
source_system: Mapped[str] = mapped_column(ForeignKey("data_source.key"), index=True)
|
|
source_url: Mapped[str] = mapped_column(Text)
|
|
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)
|
|
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
status: Mapped[str] = mapped_column(String(30), index=True)
|
|
rows_seen: Mapped[int] = mapped_column(default=0)
|
|
rows_created: Mapped[int] = mapped_column(default=0)
|
|
rows_updated: Mapped[int] = mapped_column(default=0)
|
|
error_summary: Mapped[str | None] = mapped_column(Text)
|
|
|
|
|
|
class ExternalObservation(Base):
|
|
__tablename__ = "external_observation"
|
|
__table_args__ = (UniqueConstraint("source_system", "source_external_id"),)
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
source_system: Mapped[str] = mapped_column(ForeignKey("data_source.key"), index=True)
|
|
source_external_id: Mapped[str] = mapped_column(String(200))
|
|
source_url: Mapped[str] = mapped_column(Text)
|
|
fish_name: Mapped[str] = mapped_column(String(200))
|
|
fish_external_id: Mapped[str | None] = mapped_column(String(200))
|
|
waterbody_name: Mapped[str] = mapped_column(String(200))
|
|
waterbody_external_id: Mapped[str | None] = mapped_column(String(200))
|
|
x: Mapped[int | None]
|
|
y: Mapped[int | None]
|
|
weight_g: Mapped[int | None]
|
|
published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
first_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
last_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)
|
|
status: Mapped[str] = mapped_column(String(30), default="staged", index=True)
|
|
payload: Mapped[dict] = mapped_column(JSON)
|
|
fish_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("fish.id"))
|
|
waterbody_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("waterbody.id"))
|
|
catch_report_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("catch_report.id"), unique=True)
|
|
review_note: Mapped[str | None] = mapped_column(Text)
|
|
reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
source: Mapped[DataSource] = relationship()
|
|
fish: Mapped[Fish | None] = relationship()
|
|
waterbody: Mapped[Waterbody | None] = relationship()
|
|
catch_report: Mapped[CatchReport | None] = relationship()
|
|
|
|
|
|
class ExternalEntityAlias(Base):
|
|
__tablename__ = "external_entity_alias"
|
|
__table_args__ = (
|
|
UniqueConstraint("source_system", "entity_type", "external_id"),
|
|
CheckConstraint(
|
|
"(entity_type = 'fish' AND fish_id IS NOT NULL AND waterbody_id IS NULL) OR "
|
|
"(entity_type = 'waterbody' AND waterbody_id IS NOT NULL AND fish_id IS NULL)",
|
|
name="ck_external_entity_alias_target",
|
|
),
|
|
)
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
source_system: Mapped[str] = mapped_column(ForeignKey("data_source.key"), index=True)
|
|
entity_type: Mapped[str] = mapped_column(String(20))
|
|
external_id: Mapped[str] = mapped_column(String(200))
|
|
external_name: Mapped[str] = mapped_column(String(200))
|
|
fish_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("fish.id"))
|
|
waterbody_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("waterbody.id"))
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
fish: Mapped[Fish | None] = relationship()
|
|
waterbody: Mapped[Waterbody | None] = relationship()
|
|
|
|
|
|
class ImportRecordEvent(Base):
|
|
"""Track per-record import events for D09 revision history."""
|
|
__tablename__ = "import_record_event"
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
catch_report_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("catch_report.id"), index=True)
|
|
import_run_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("official_record_import.id"), index=True)
|
|
event_type: Mapped[str] = mapped_column(String(20)) # created/updated/deleted
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
catch_report: Mapped[CatchReport] = relationship()
|
|
import_run: Mapped[OfficialRecordImport] = relationship()
|