feat: add evaluated_only filter to GET /api/v2/refractor/cards (#174) #175
@ -107,6 +107,7 @@ async def list_card_states(
|
|||||||
tier: Optional[int] = Query(default=None, ge=0, le=4),
|
tier: Optional[int] = Query(default=None, ge=0, le=4),
|
||||||
season: Optional[int] = Query(default=None),
|
season: Optional[int] = Query(default=None),
|
||||||
progress: Optional[str] = Query(default=None),
|
progress: Optional[str] = Query(default=None),
|
||||||
|
evaluated_only: bool = Query(default=True),
|
||||||
limit: int = Query(default=10, ge=1, le=100),
|
limit: int = Query(default=10, ge=1, le=100),
|
||||||
offset: int = Query(default=0, ge=0),
|
offset: int = Query(default=0, ge=0),
|
||||||
token: str = Depends(oauth2_scheme),
|
token: str = Depends(oauth2_scheme),
|
||||||
@ -114,15 +115,18 @@ async def list_card_states(
|
|||||||
"""List RefractorCardState rows for a team, with optional filters and pagination.
|
"""List RefractorCardState rows for a team, with optional filters and pagination.
|
||||||
|
|
||||||
Required:
|
Required:
|
||||||
team_id -- filter to this team's cards; returns empty list if team has no states
|
team_id -- filter to this team's cards; returns empty list if team has no states
|
||||||
|
|
||||||
Optional filters:
|
Optional filters:
|
||||||
card_type -- one of 'batter', 'sp', 'rp'; filters by RefractorTrack.card_type
|
card_type -- one of 'batter', 'sp', 'rp'; filters by RefractorTrack.card_type
|
||||||
tier -- filter by current_tier (0-4)
|
tier -- filter by current_tier (0-4)
|
||||||
season -- filter to players who have batting or pitching season stats in that
|
season -- filter to players who have batting or pitching season stats in that
|
||||||
season (EXISTS subquery against batting/pitching_season_stats)
|
season (EXISTS subquery against batting/pitching_season_stats)
|
||||||
progress -- 'close' = only cards within 80% of their next tier threshold;
|
progress -- 'close' = only cards within 80% of their next tier threshold;
|
||||||
fully evolved cards are always excluded from this filter
|
fully evolved cards are always excluded from this filter
|
||||||
|
evaluated_only -- default True; when True, excludes cards where last_evaluated_at
|
||||||
|
is NULL (cards created but never run through the evaluator).
|
||||||
|
Set to False to include all rows, including zero-value placeholders.
|
||||||
|
|
||||||
Pagination:
|
Pagination:
|
||||||
limit -- page size (1-100, default 10)
|
limit -- page size (1-100, default 10)
|
||||||
@ -199,6 +203,9 @@ async def list_card_states(
|
|||||||
& (RefractorCardState.current_value >= next_threshold_expr * 0.8)
|
& (RefractorCardState.current_value >= next_threshold_expr * 0.8)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if evaluated_only:
|
||||||
|
query = query.where(RefractorCardState.last_evaluated_at.is_null(False))
|
||||||
|
|
||||||
total = query.count()
|
total = query.count()
|
||||||
items = []
|
items = []
|
||||||
for state in query.offset(offset).limit(limit):
|
for state in query.offset(offset).limit(limit):
|
||||||
|
|||||||
@ -956,3 +956,83 @@ def test_get_card_state_last_evaluated_at_null(setup_state_api_db, state_api_cli
|
|||||||
f"Expected last_evaluated_at=null for un-evaluated card, "
|
f"Expected last_evaluated_at=null for un-evaluated card, "
|
||||||
f"got {data['last_evaluated_at']!r}"
|
f"got {data['last_evaluated_at']!r}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# T3-8: GET /refractor/cards?team_id=X&evaluated_only=false includes un-evaluated
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_cards_evaluated_only_false_includes_unevaluated(
|
||||||
|
setup_state_api_db, state_api_client
|
||||||
|
):
|
||||||
|
"""GET /refractor/cards?team_id=X&evaluated_only=false returns cards with last_evaluated_at=NULL.
|
||||||
|
|
||||||
|
What: Create two RefractorCardState rows for the same team — one with
|
||||||
|
last_evaluated_at=None (never evaluated) and one with last_evaluated_at set
|
||||||
|
to a timestamp (has been evaluated). Call the endpoint twice:
|
||||||
|
1. evaluated_only=true (default) — only the evaluated card appears.
|
||||||
|
2. evaluated_only=false — both cards appear.
|
||||||
|
|
||||||
|
Why: The default evaluated_only=True filter uses
|
||||||
|
`last_evaluated_at IS NOT NULL` to exclude placeholder rows created at
|
||||||
|
pack-open time but never run through the evaluator. At team scale (2753
|
||||||
|
rows, ~14 evaluated) this filter is critical for bot performance.
|
||||||
|
This test verifies the opt-out path (evaluated_only=false) exposes all rows,
|
||||||
|
which is needed for admin/pipeline use cases.
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
team = _sa_make_team("SA_T38", gmid=30380)
|
||||||
|
track = _sa_make_track("batter")
|
||||||
|
|
||||||
|
# Card 1: never evaluated — last_evaluated_at is NULL
|
||||||
|
player_unevaluated = _sa_make_player("T38 Unevaluated", pos="1B")
|
||||||
|
card_unevaluated = _sa_make_card(player_unevaluated, team) # noqa: F841
|
||||||
|
RefractorCardState.create(
|
||||||
|
player=player_unevaluated,
|
||||||
|
team=team,
|
||||||
|
track=track,
|
||||||
|
current_tier=0,
|
||||||
|
current_value=0.0,
|
||||||
|
fully_evolved=False,
|
||||||
|
last_evaluated_at=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Card 2: has been evaluated — last_evaluated_at is a timestamp
|
||||||
|
player_evaluated = _sa_make_player("T38 Evaluated", pos="RF")
|
||||||
|
card_evaluated = _sa_make_card(player_evaluated, team) # noqa: F841
|
||||||
|
RefractorCardState.create(
|
||||||
|
player=player_evaluated,
|
||||||
|
team=team,
|
||||||
|
track=track,
|
||||||
|
current_tier=1,
|
||||||
|
current_value=5.0,
|
||||||
|
fully_evolved=False,
|
||||||
|
last_evaluated_at=datetime(2025, 4, 1, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Default (evaluated_only=true) — only the evaluated card should appear
|
||||||
|
resp_default = state_api_client.get(
|
||||||
|
f"/api/v2/refractor/cards?team_id={team.id}", headers=AUTH_HEADER
|
||||||
|
)
|
||||||
|
assert resp_default.status_code == 200
|
||||||
|
data_default = resp_default.json()
|
||||||
|
assert data_default["count"] == 1, (
|
||||||
|
f"evaluated_only=true should return 1 card, got {data_default['count']}"
|
||||||
|
)
|
||||||
|
assert data_default["items"][0]["player_name"] == "T38 Evaluated"
|
||||||
|
|
||||||
|
# evaluated_only=false — both cards should appear
|
||||||
|
resp_all = state_api_client.get(
|
||||||
|
f"/api/v2/refractor/cards?team_id={team.id}&evaluated_only=false",
|
||||||
|
headers=AUTH_HEADER,
|
||||||
|
)
|
||||||
|
assert resp_all.status_code == 200
|
||||||
|
data_all = resp_all.json()
|
||||||
|
assert data_all["count"] == 2, (
|
||||||
|
f"evaluated_only=false should return 2 cards, got {data_all['count']}"
|
||||||
|
)
|
||||||
|
names = {item["player_name"] for item in data_all["items"]}
|
||||||
|
assert "T38 Unevaluated" in names
|
||||||
|
assert "T38 Evaluated" in names
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user