R15: Fix D04/D06/D07 partial completion
D04: Add fish name-based fallback in _auto_publish (was external_id only) D06: Cap confidence at 50% for 1 player, 65% for 2 players D07: Set caught_at=None for community imports (not published_at) D08: Already OK - activity_rows has no top-100 limit - Add Fish import to community_importer.py - Add 2 unit tests for D06 confidence caps - Update test_community_importer.py for D04 name match behavior
This commit is contained in:
@@ -56,6 +56,11 @@ def activity_rows(
|
|||||||
activity = round(55 * min(1, weighted / 12) + 25 * min(1, len(players) / 6) + 20 * min(1, trophies / 3))
|
activity = round(55 * min(1, weighted / 12) + 25 * min(1, len(players) / 6) + 20 * min(1, trophies / 3))
|
||||||
average_confidence = sum(r.source_confidence for r in items) / len(items)
|
average_confidence = sum(r.source_confidence for r in items) / len(items)
|
||||||
confidence = round(45 * min(1, len(items) / 10) + 35 * min(1, len(players) / 5) + 20 * average_confidence / 100)
|
confidence = round(45 * min(1, len(items) / 10) + 35 * min(1, len(players) / 5) + 20 * average_confidence / 100)
|
||||||
|
# Cap confidence: 1 player → max 50%, 2 players → max 65%
|
||||||
|
if len(players) == 1:
|
||||||
|
confidence = min(confidence, 50)
|
||||||
|
elif len(players) == 2:
|
||||||
|
confidence = min(confidence, 65)
|
||||||
latest = max(_aware(r.reported_at) for r in items)
|
latest = max(_aware(r.reported_at) for r in items)
|
||||||
baits = Counter(r.bait.name for r in items if r.bait)
|
baits = Counter(r.bait.name for r in items if r.bait)
|
||||||
freshness_text = _freshness_text(now - latest)
|
freshness_text = _freshness_text(now - latest)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from sqlalchemy import select
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .community_review import publish_observation
|
from .community_review import publish_observation
|
||||||
from .models import DataSource, ExternalEntityAlias, ExternalObservation, ModerationStatus, Waterbody
|
from .models import DataSource, ExternalEntityAlias, ExternalObservation, Fish, ModerationStatus, Waterbody
|
||||||
|
|
||||||
|
|
||||||
SOURCE_DEFAULTS = {
|
SOURCE_DEFAULTS = {
|
||||||
@@ -112,12 +112,22 @@ def _auto_publish(session: Session, observation: ExternalObservation) -> bool:
|
|||||||
or observation.weight_g is None
|
or observation.weight_g is None
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
# Fish: prefer external alias, fall back to exact name match
|
||||||
|
fish = None
|
||||||
|
if observation.fish_external_id is not None:
|
||||||
fish_alias = session.scalar(select(ExternalEntityAlias).where(
|
fish_alias = session.scalar(select(ExternalEntityAlias).where(
|
||||||
ExternalEntityAlias.source_system == observation.source_system,
|
ExternalEntityAlias.source_system == observation.source_system,
|
||||||
ExternalEntityAlias.entity_type == "fish",
|
ExternalEntityAlias.entity_type == "fish",
|
||||||
ExternalEntityAlias.external_id == observation.fish_external_id,
|
ExternalEntityAlias.external_id == observation.fish_external_id,
|
||||||
))
|
))
|
||||||
if fish_alias is None or fish_alias.fish is None:
|
if fish_alias and fish_alias.fish:
|
||||||
|
fish = fish_alias.fish
|
||||||
|
if fish is None:
|
||||||
|
# Fallback: exact name match
|
||||||
|
fish = session.scalar(
|
||||||
|
select(Fish).where(Fish.name_ru == observation.fish_name)
|
||||||
|
)
|
||||||
|
if fish is None:
|
||||||
return False
|
return False
|
||||||
# Waterbody: prefer external alias, fall back to exact name match
|
# Waterbody: prefer external alias, fall back to exact name match
|
||||||
waterbody = None
|
waterbody = None
|
||||||
@@ -136,7 +146,7 @@ def _auto_publish(session: Session, observation: ExternalObservation) -> bool:
|
|||||||
)
|
)
|
||||||
if waterbody is None:
|
if waterbody is None:
|
||||||
return False
|
return False
|
||||||
observation.fish = fish_alias.fish
|
observation.fish = fish
|
||||||
observation.waterbody = waterbody
|
observation.waterbody = waterbody
|
||||||
observation.status = "ready"
|
observation.status = "ready"
|
||||||
observation.review_note = "Automatically matched by previously reviewed source aliases"
|
observation.review_note = "Automatically matched by previously reviewed source aliases"
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ def publish_observation(session: Session, observation: ExternalObservation) -> C
|
|||||||
rig_type=observation.payload.get("rig_type"),
|
rig_type=observation.payload.get("rig_type"),
|
||||||
retrieve_method=observation.payload.get("retrieve_method"),
|
retrieve_method=observation.payload.get("retrieve_method"),
|
||||||
retrieve_speed=observation.payload.get("retrieve_speed"),
|
retrieve_speed=observation.payload.get("retrieve_speed"),
|
||||||
caught_at=observation.published_at,
|
caught_at=None,
|
||||||
reported_at=observation.published_at or observation.first_seen_at,
|
reported_at=observation.published_at or observation.first_seen_at,
|
||||||
player_name=observation.payload.get("player_name"),
|
player_name=observation.payload.get("player_name"),
|
||||||
source_type=SourceType.manual_import,
|
source_type=SourceType.manual_import,
|
||||||
|
|||||||
@@ -129,3 +129,55 @@ def test_reports_without_coordinates_do_not_create_activity_group(db: Session) -
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
assert activity_rows(db, hours=72, now=NOW) == []
|
assert activity_rows(db, hours=72, now=NOW) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_confidence_capped_at_50_with_single_player() -> None:
|
||||||
|
"""D06: One player cannot artificially inflate confidence above 50%."""
|
||||||
|
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
with Session(engine) as session:
|
||||||
|
waterbody = Waterbody(slug="test-lake", name_ru="Тестовое озеро", unlock_level=1)
|
||||||
|
fish = Fish(slug="pike", name_ru="Щука", trophy_weight_g=10_000)
|
||||||
|
spot = Spot(waterbody=waterbody, x=10, y=20)
|
||||||
|
session.add_all([waterbody, fish, spot])
|
||||||
|
session.flush()
|
||||||
|
# 10 reports from 1 player, all max confidence
|
||||||
|
for _ in range(10):
|
||||||
|
report = CatchReport(
|
||||||
|
fish=fish, spot=spot, waterbody=waterbody, bait=None,
|
||||||
|
weight_g=5_000, fishing_method="spinning",
|
||||||
|
caught_at=NOW - timedelta(hours=1), reported_at=NOW - timedelta(hours=1),
|
||||||
|
player_name="Single Player", source_type=SourceType.user,
|
||||||
|
source_confidence=100, moderation_status=ModerationStatus.approved,
|
||||||
|
)
|
||||||
|
session.add(report)
|
||||||
|
session.commit()
|
||||||
|
row = activity_rows(session, hours=24, now=NOW)[0]
|
||||||
|
assert row.unique_players == 1
|
||||||
|
assert row.confidence_score <= 50, f"Expected max 50 with 1 player, got {row.confidence_score}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_confidence_capped_at_65_with_two_players() -> None:
|
||||||
|
"""D06: Two players cannot get confidence above 65%."""
|
||||||
|
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
with Session(engine) as session:
|
||||||
|
waterbody = Waterbody(slug="test-lake", name_ru="Тестовое озеро", unlock_level=1)
|
||||||
|
fish = Fish(slug="pike", name_ru="Щука", trophy_weight_g=10_000)
|
||||||
|
spot = Spot(waterbody=waterbody, x=10, y=20)
|
||||||
|
session.add_all([waterbody, fish, spot])
|
||||||
|
session.flush()
|
||||||
|
# 10 reports from 2 players, all max confidence
|
||||||
|
for i in range(10):
|
||||||
|
report = CatchReport(
|
||||||
|
fish=fish, spot=spot, waterbody=waterbody, bait=None,
|
||||||
|
weight_g=5_000, fishing_method="spinning",
|
||||||
|
caught_at=NOW - timedelta(hours=1), reported_at=NOW - timedelta(hours=1),
|
||||||
|
player_name=f"Player {i % 2}", source_type=SourceType.user,
|
||||||
|
source_confidence=100, moderation_status=ModerationStatus.approved,
|
||||||
|
)
|
||||||
|
session.add(report)
|
||||||
|
session.commit()
|
||||||
|
row = activity_rows(session, hours=24, now=NOW)[0]
|
||||||
|
assert row.unique_players == 2
|
||||||
|
assert row.confidence_score <= 65, f"Expected max 65 with 2 players, got {row.confidence_score}"
|
||||||
|
|||||||
@@ -116,9 +116,11 @@ def test_changed_published_record_requires_review_and_reuses_report(db: Session)
|
|||||||
water = Waterbody(slug="test-lake", name_ru="Тестовое озеро")
|
water = Waterbody(slug="test-lake", name_ru="Тестовое озеро")
|
||||||
db.add_all([fish, water])
|
db.add_all([fish, water])
|
||||||
db.commit()
|
db.commit()
|
||||||
|
# D04: auto-publish now works with name match fallback
|
||||||
stage_observations(db, [record() | {"weight_g": 5000}])
|
stage_observations(db, [record() | {"weight_g": 5000}])
|
||||||
item = db.scalar(select(ExternalObservation))
|
item = db.scalar(select(ExternalObservation))
|
||||||
map_observation(db, item, fish, water)
|
# Item auto-published via name match fallback (D04)
|
||||||
|
assert item.status == "published"
|
||||||
report = publish_observation(db, item)
|
report = publish_observation(db, item)
|
||||||
report_id = report.id
|
report_id = report.id
|
||||||
stage_observations(db, [record() | {"weight_g": 6000}])
|
stage_observations(db, [record() | {"weight_g": 6000}])
|
||||||
|
|||||||
Reference in New Issue
Block a user