fix: event-log route handles missing location_id
The /event-log route now shows a location selector dropdown instead of returning a 422 error when no location_id is provided. This follows the same pattern used by the Eggs page. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ from starlette.requests import Request
|
|||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
from animaltrack.repositories.locations import LocationRepository
|
from animaltrack.repositories.locations import LocationRepository
|
||||||
|
from animaltrack.repositories.user_defaults import UserDefaultsRepository
|
||||||
from animaltrack.web.templates import page
|
from animaltrack.web.templates import page
|
||||||
from animaltrack.web.templates.events import event_log_list, event_log_panel
|
from animaltrack.web.templates.events import event_log_list, event_log_panel
|
||||||
|
|
||||||
@@ -56,24 +57,35 @@ def event_log_index(request: Request):
|
|||||||
"""GET /event-log - Event log for a location."""
|
"""GET /event-log - Event log for a location."""
|
||||||
db = request.app.state.db
|
db = request.app.state.db
|
||||||
|
|
||||||
|
# Get auth info
|
||||||
|
auth = request.scope.get("auth")
|
||||||
|
username = auth.username if auth else None
|
||||||
|
user_role = auth.role if auth else None
|
||||||
|
|
||||||
# Get location_id from query params
|
# Get location_id from query params
|
||||||
location_id = request.query_params.get("location_id")
|
location_id = request.query_params.get("location_id")
|
||||||
if not location_id:
|
|
||||||
return HTMLResponse(
|
|
||||||
content="<p>Missing location_id parameter</p>",
|
|
||||||
status_code=422,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get location name
|
# If no query param, try user defaults
|
||||||
|
if not location_id and username:
|
||||||
|
defaults = UserDefaultsRepository(db).get(username, "event_log")
|
||||||
|
if defaults:
|
||||||
|
location_id = defaults.location_id
|
||||||
|
|
||||||
|
# Get all locations for selector
|
||||||
location_repo = LocationRepository(db)
|
location_repo = LocationRepository(db)
|
||||||
locations = location_repo.list_active()
|
locations = location_repo.list_active()
|
||||||
location_name = "Unknown"
|
|
||||||
|
# Find location name if we have a location_id
|
||||||
|
location_name = None
|
||||||
|
if location_id:
|
||||||
for loc in locations:
|
for loc in locations:
|
||||||
if loc.id == location_id:
|
if loc.id == location_id:
|
||||||
location_name = loc.name
|
location_name = loc.name
|
||||||
break
|
break
|
||||||
|
|
||||||
# Get event log
|
# Get event log if we have a valid location
|
||||||
|
events = []
|
||||||
|
if location_id and location_name:
|
||||||
events = get_event_log(db, location_id)
|
events = get_event_log(db, location_id)
|
||||||
|
|
||||||
# Check if HTMX request
|
# Check if HTMX request
|
||||||
@@ -85,9 +97,11 @@ def event_log_index(request: Request):
|
|||||||
|
|
||||||
# Full page render
|
# Full page render
|
||||||
return page(
|
return page(
|
||||||
event_log_panel(events, location_name),
|
event_log_panel(events, locations, location_id),
|
||||||
title=f"Event Log - {location_name}",
|
title="Event Log - AnimalTrack",
|
||||||
active_nav=None,
|
active_nav="event_log",
|
||||||
|
user_role=user_role,
|
||||||
|
username=username,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fasthtml.common import H3, Div, Li, P, Span, Ul
|
from fasthtml.common import H3, Div, Label, Li, Option, P, Select, Span, Ul
|
||||||
|
|
||||||
|
|
||||||
def format_timestamp(ts_utc: int) -> str:
|
def format_timestamp(ts_utc: int) -> str:
|
||||||
@@ -120,11 +120,55 @@ def event_log_list(events: list[dict[str, Any]]) -> Any:
|
|||||||
return Ul(*items, cls="divide-y divide-stone-200")
|
return Ul(*items, cls="divide-y divide-stone-200")
|
||||||
|
|
||||||
|
|
||||||
def event_log_panel(events: list[dict[str, Any]], location_name: str) -> Any:
|
def location_selector(locations: list[Any], selected_location_id: str | None) -> Any:
|
||||||
"""Render the full event log panel."""
|
"""Render location selector dropdown."""
|
||||||
|
options = [Option("Select a location...", value="", selected=not selected_location_id)]
|
||||||
|
for loc in locations:
|
||||||
|
options.append(Option(loc.name, value=loc.id, selected=loc.id == selected_location_id))
|
||||||
|
|
||||||
return Div(
|
return Div(
|
||||||
H3(f"Event Log - {location_name}", cls="text-lg font-semibold mb-4"),
|
Label("Location", cls="text-sm font-medium text-stone-700"),
|
||||||
event_log_list(events),
|
Select(
|
||||||
|
*options,
|
||||||
|
name="location_id",
|
||||||
|
cls="mt-1 block w-full rounded-md border-stone-300 shadow-sm "
|
||||||
|
"focus:border-amber-500 focus:ring-amber-500 sm:text-sm",
|
||||||
|
hx_get="/event-log",
|
||||||
|
hx_trigger="change",
|
||||||
|
hx_target="#event-log-content",
|
||||||
|
hx_swap="innerHTML",
|
||||||
|
hx_include="this",
|
||||||
|
),
|
||||||
|
cls="mb-4 max-w-xs",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def event_log_panel(
|
||||||
|
events: list[dict[str, Any]], locations: list[Any], selected_location_id: str | None
|
||||||
|
) -> Any:
|
||||||
|
"""Render the full event log panel."""
|
||||||
|
# Find location name for header
|
||||||
|
location_name = None
|
||||||
|
if selected_location_id:
|
||||||
|
for loc in locations:
|
||||||
|
if loc.id == selected_location_id:
|
||||||
|
location_name = loc.name
|
||||||
|
break
|
||||||
|
|
||||||
|
header_text = f"Event Log - {location_name}" if location_name else "Event Log"
|
||||||
|
|
||||||
|
return Div(
|
||||||
|
H3(header_text, cls="text-lg font-semibold mb-4"),
|
||||||
|
location_selector(locations, selected_location_id),
|
||||||
|
Div(
|
||||||
|
event_log_list(events)
|
||||||
|
if selected_location_id
|
||||||
|
else P(
|
||||||
|
"Select a location to view events.",
|
||||||
|
cls="text-stone-500 text-sm text-center py-4",
|
||||||
|
),
|
||||||
|
id="event-log-content",
|
||||||
|
),
|
||||||
cls="bg-white rounded-lg shadow p-4",
|
cls="bg-white rounded-lg shadow p-4",
|
||||||
id="event-log",
|
id="event-log",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -107,10 +107,12 @@ def create_cohort(animal_service, location_id, count=3):
|
|||||||
class TestEventLogRoute:
|
class TestEventLogRoute:
|
||||||
"""Tests for GET /event-log route."""
|
"""Tests for GET /event-log route."""
|
||||||
|
|
||||||
def test_event_log_requires_location_id(self, client):
|
def test_event_log_without_location_shows_selector(self, client):
|
||||||
"""Event log requires location_id parameter."""
|
"""Event log without location_id shows location selector."""
|
||||||
response = client.get("/event-log")
|
response = client.get("/event-log")
|
||||||
assert response.status_code == 422
|
assert response.status_code == 200
|
||||||
|
# Should show location selector prompt
|
||||||
|
assert "Select a location" in response.text
|
||||||
|
|
||||||
def test_event_log_returns_empty_for_new_location(self, client, valid_location_id):
|
def test_event_log_returns_empty_for_new_location(self, client, valid_location_id):
|
||||||
"""Event log returns empty state for location with no events."""
|
"""Event log returns empty state for location with no events."""
|
||||||
|
|||||||
Reference in New Issue
Block a user