Add recent events and stats to eggs, feed, and move forms
All checks were successful
Deploy / deploy (push) Successful in 2m40s
All checks were successful
Deploy / deploy (push) Successful in 2m40s
- Create recent_events.py helper for rendering event lists with humanized timestamps and deleted event styling (line-through + opacity) - Query events with ORDER BY ts_utc DESC to show newest first - Join event_tombstones to detect deleted events - Fix move form to read animal_ids (not resolved_ids) from entity_refs - Fix feed purchase format to use total_kg from entity_refs - Use hx_get with #event-panel-content target for slide-over panel - Add days-since-last stats for move and feed forms 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -211,3 +211,60 @@ class TestEggCollection:
|
||||
# The response should contain the form with the location pre-selected
|
||||
# Check for "selected" attribute on the option with our location_id
|
||||
assert "selected" in resp.text and location_strip1_id in resp.text
|
||||
|
||||
|
||||
class TestEggsRecentEvents:
|
||||
"""Tests for recent events display on eggs page."""
|
||||
|
||||
def test_harvest_tab_shows_recent_events_section(self, client):
|
||||
"""Harvest tab shows Recent Harvests section."""
|
||||
resp = client.get("/")
|
||||
assert resp.status_code == 200
|
||||
assert "Recent Harvests" in resp.text
|
||||
|
||||
def test_sell_tab_shows_recent_events_section(self, client):
|
||||
"""Sell tab shows Recent Sales section."""
|
||||
resp = client.get("/?tab=sell")
|
||||
assert resp.status_code == 200
|
||||
assert "Recent Sales" in resp.text
|
||||
|
||||
def test_harvest_event_appears_in_recent(
|
||||
self, client, seeded_db, location_strip1_id, ducks_at_strip1
|
||||
):
|
||||
"""Newly created harvest event appears in recent events list."""
|
||||
resp = client.post(
|
||||
"/actions/product-collected",
|
||||
data={
|
||||
"location_id": location_strip1_id,
|
||||
"quantity": "12",
|
||||
"nonce": "test-nonce-recent-1",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
# Recent events should include the newly created event
|
||||
# Check for event link pattern
|
||||
assert "/events/" in resp.text
|
||||
|
||||
def test_harvest_event_links_to_detail(
|
||||
self, client, seeded_db, location_strip1_id, ducks_at_strip1
|
||||
):
|
||||
"""Harvest events in recent list link to event detail page."""
|
||||
# Create an event
|
||||
resp = client.post(
|
||||
"/actions/product-collected",
|
||||
data={
|
||||
"location_id": location_strip1_id,
|
||||
"quantity": "8",
|
||||
"nonce": "test-nonce-recent-2",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Get the event ID from DB
|
||||
event_row = seeded_db.execute(
|
||||
"SELECT id FROM events WHERE type = 'ProductCollected' ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
event_id = event_row[0]
|
||||
|
||||
# The response should contain a link to the event detail
|
||||
assert f"/events/{event_id}" in resp.text
|
||||
|
||||
@@ -360,3 +360,99 @@ class TestInventoryWarning:
|
||||
assert resp.status_code in [200, 302, 303]
|
||||
# The response should contain a warning about negative inventory
|
||||
assert "warning" in resp.text.lower() or "negative" in resp.text.lower()
|
||||
|
||||
|
||||
class TestFeedRecentEvents:
|
||||
"""Tests for recent events display on feed page."""
|
||||
|
||||
def test_give_tab_shows_recent_events_section(self, client):
|
||||
"""Give Feed tab shows Recent Feed Given section."""
|
||||
resp = client.get("/feed")
|
||||
assert resp.status_code == 200
|
||||
assert "Recent Feed Given" in resp.text
|
||||
|
||||
def test_purchase_tab_shows_recent_events_section(self, client):
|
||||
"""Purchase Feed tab shows Recent Purchases section."""
|
||||
resp = client.get("/feed?tab=purchase")
|
||||
assert resp.status_code == 200
|
||||
assert "Recent Purchases" in resp.text
|
||||
|
||||
def test_give_feed_event_appears_in_recent(
|
||||
self, client, seeded_db, location_strip1_id, feed_purchase_in_db
|
||||
):
|
||||
"""Newly created feed given event appears in recent events list."""
|
||||
resp = client.post(
|
||||
"/actions/feed-given",
|
||||
data={
|
||||
"location_id": location_strip1_id,
|
||||
"feed_type_code": "layer",
|
||||
"amount_kg": "5",
|
||||
"nonce": "test-nonce-recent-feed-1",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
# Recent events should include the newly created event
|
||||
assert "/events/" in resp.text
|
||||
|
||||
def test_give_feed_event_links_to_detail(
|
||||
self, client, seeded_db, location_strip1_id, feed_purchase_in_db
|
||||
):
|
||||
"""Feed given events in recent list link to event detail page."""
|
||||
resp = client.post(
|
||||
"/actions/feed-given",
|
||||
data={
|
||||
"location_id": location_strip1_id,
|
||||
"feed_type_code": "layer",
|
||||
"amount_kg": "5",
|
||||
"nonce": "test-nonce-recent-feed-2",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Get the event ID from DB
|
||||
event_row = seeded_db.execute(
|
||||
"SELECT id FROM events WHERE type = 'FeedGiven' ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
event_id = event_row[0]
|
||||
|
||||
# The response should contain a link to the event detail
|
||||
assert f"/events/{event_id}" in resp.text
|
||||
|
||||
def test_purchase_event_appears_in_recent(self, client, seeded_db):
|
||||
"""Newly created purchase event appears in recent events list."""
|
||||
resp = client.post(
|
||||
"/actions/feed-purchased",
|
||||
data={
|
||||
"feed_type_code": "layer",
|
||||
"bag_size_kg": "20",
|
||||
"bags_count": "2",
|
||||
"bag_price_euros": "24.00",
|
||||
"nonce": "test-nonce-recent-purchase-1",
|
||||
},
|
||||
)
|
||||
# The route returns purchase tab active after purchase
|
||||
assert resp.status_code == 200
|
||||
assert "/events/" in resp.text
|
||||
|
||||
def test_purchase_event_links_to_detail(self, client, seeded_db):
|
||||
"""Purchase events in recent list link to event detail page."""
|
||||
resp = client.post(
|
||||
"/actions/feed-purchased",
|
||||
data={
|
||||
"feed_type_code": "layer",
|
||||
"bag_size_kg": "20",
|
||||
"bags_count": "2",
|
||||
"bag_price_euros": "24.00",
|
||||
"nonce": "test-nonce-recent-purchase-2",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Get the event ID from DB
|
||||
event_row = seeded_db.execute(
|
||||
"SELECT id FROM events WHERE type = 'FeedPurchased' ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
event_id = event_row[0]
|
||||
|
||||
# The response should contain a link to the event detail
|
||||
assert f"/events/{event_id}" in resp.text
|
||||
|
||||
@@ -472,3 +472,116 @@ class TestMoveAnimalMismatch:
|
||||
payload = json.loads(event_row[0])
|
||||
# Should have moved 3 animals (5 original - 2 moved by client B)
|
||||
assert len(payload["resolved_ids"]) == 3
|
||||
|
||||
|
||||
class TestMoveRecentEvents:
|
||||
"""Tests for recent events display on move page."""
|
||||
|
||||
def test_move_form_shows_recent_events_section(self, client):
|
||||
"""Move form shows Recent Moves section."""
|
||||
resp = client.get("/move")
|
||||
assert resp.status_code == 200
|
||||
assert "Recent Moves" in resp.text
|
||||
|
||||
def test_move_event_appears_in_recent(
|
||||
self,
|
||||
client,
|
||||
seeded_db,
|
||||
animal_service,
|
||||
location_strip1_id,
|
||||
location_strip2_id,
|
||||
ducks_at_strip1,
|
||||
):
|
||||
"""Newly created move event appears in recent events list."""
|
||||
ts_utc = int(time.time() * 1000)
|
||||
filter_str = 'location:"Strip 1"'
|
||||
filter_ast = parse_filter(filter_str)
|
||||
resolution = resolve_filter(seeded_db, filter_ast, ts_utc)
|
||||
roster_hash = compute_roster_hash(resolution.animal_ids, location_strip1_id)
|
||||
|
||||
resp = client.post(
|
||||
"/actions/animal-move",
|
||||
data={
|
||||
"filter": filter_str,
|
||||
"to_location_id": location_strip2_id,
|
||||
"resolved_ids": resolution.animal_ids,
|
||||
"roster_hash": roster_hash,
|
||||
"from_location_id": location_strip1_id,
|
||||
"ts_utc": str(ts_utc),
|
||||
"nonce": "test-nonce-recent-move-1",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
# Recent events should include the newly created event
|
||||
assert "/events/" in resp.text
|
||||
|
||||
def test_move_event_links_to_detail(
|
||||
self,
|
||||
client,
|
||||
seeded_db,
|
||||
animal_service,
|
||||
location_strip1_id,
|
||||
location_strip2_id,
|
||||
ducks_at_strip1,
|
||||
):
|
||||
"""Move events in recent list link to event detail page."""
|
||||
ts_utc = int(time.time() * 1000)
|
||||
filter_str = 'location:"Strip 1"'
|
||||
filter_ast = parse_filter(filter_str)
|
||||
resolution = resolve_filter(seeded_db, filter_ast, ts_utc)
|
||||
roster_hash = compute_roster_hash(resolution.animal_ids, location_strip1_id)
|
||||
|
||||
resp = client.post(
|
||||
"/actions/animal-move",
|
||||
data={
|
||||
"filter": filter_str,
|
||||
"to_location_id": location_strip2_id,
|
||||
"resolved_ids": resolution.animal_ids,
|
||||
"roster_hash": roster_hash,
|
||||
"from_location_id": location_strip1_id,
|
||||
"ts_utc": str(ts_utc),
|
||||
"nonce": "test-nonce-recent-move-2",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Get the event ID from DB
|
||||
event_row = seeded_db.execute(
|
||||
"SELECT id FROM events WHERE type = 'AnimalMoved' ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
event_id = event_row[0]
|
||||
|
||||
# The response should contain a link to the event detail
|
||||
assert f"/events/{event_id}" in resp.text
|
||||
|
||||
def test_days_since_last_move_shows_today(
|
||||
self,
|
||||
client,
|
||||
seeded_db,
|
||||
animal_service,
|
||||
location_strip1_id,
|
||||
location_strip2_id,
|
||||
ducks_at_strip1,
|
||||
):
|
||||
"""After a move today, shows 'Last move: today'."""
|
||||
ts_utc = int(time.time() * 1000)
|
||||
filter_str = 'location:"Strip 1"'
|
||||
filter_ast = parse_filter(filter_str)
|
||||
resolution = resolve_filter(seeded_db, filter_ast, ts_utc)
|
||||
roster_hash = compute_roster_hash(resolution.animal_ids, location_strip1_id)
|
||||
|
||||
resp = client.post(
|
||||
"/actions/animal-move",
|
||||
data={
|
||||
"filter": filter_str,
|
||||
"to_location_id": location_strip2_id,
|
||||
"resolved_ids": resolution.animal_ids,
|
||||
"roster_hash": roster_hash,
|
||||
"from_location_id": location_strip1_id,
|
||||
"ts_utc": str(ts_utc),
|
||||
"nonce": "test-nonce-recent-move-3",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
# Stats should show "Last move: today"
|
||||
assert "Last move: today" in resp.text
|
||||
|
||||
Reference in New Issue
Block a user