refactor: move route handlers to module level for idiomatic FastHTML

- Routes are now at module level, accessible for import by templates
- Templates accept action parameter (route function or URL string)
- Routes pass themselves to templates for type-safe form actions
- Changes DB access pattern from app.state.db to request.app.state.db
- Registration uses rt(...)(func) pattern instead of @rt decorator

This enables the idiomatic FastHTML pattern where forms can use
action=route_function instead of action="/path/string", providing
type safety and refactoring support.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 11:11:08 +00:00
parent 600d5003ed
commit b1bfdfb05c
4 changed files with 454 additions and 421 deletions

View File

@@ -49,33 +49,24 @@ def resolve_ducks_at_location(db: Any, location_id: str, ts_utc: int) -> list[st
return [row[0] for row in rows] return [row[0] for row in rows]
def register_egg_routes(rt, app): def egg_index(request: Request):
"""Register egg capture routes.
Args:
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
@rt("/")
def index(request: Request):
"""GET / - Egg Quick Capture form.""" """GET / - Egg Quick Capture form."""
db = app.state.db db = request.app.state.db
locations = LocationRepository(db).list_active() locations = LocationRepository(db).list_active()
# Check for pre-selected location from query params # Check for pre-selected location from query params
selected_location_id = request.query_params.get("location_id") selected_location_id = request.query_params.get("location_id")
return page( return page(
egg_form(locations, selected_location_id=selected_location_id), egg_form(locations, selected_location_id=selected_location_id, action=product_collected),
title="Egg - AnimalTrack", title="Egg - AnimalTrack",
active_nav="egg", active_nav="egg",
) )
@rt("/actions/product-collected", methods=["POST"])
async def product_collected(request: Request): async def product_collected(request: Request):
"""POST /actions/product-collected - Record egg collection.""" """POST /actions/product-collected - Record egg collection."""
db = app.state.db db = request.app.state.db
form = await request.form() form = await request.form()
# Extract form data # Extract form data
@@ -148,7 +139,7 @@ def register_egg_routes(rt, app):
response = HTMLResponse( response = HTMLResponse(
content=str( content=str(
page( page(
egg_form(locations, selected_location_id=location_id), egg_form(locations, selected_location_id=location_id, action=product_collected),
title="Egg - AnimalTrack", title="Egg - AnimalTrack",
active_nav="egg", active_nav="egg",
) )
@@ -163,6 +154,17 @@ def register_egg_routes(rt, app):
return response return response
def register_egg_routes(rt, app):
"""Register egg capture routes.
Args:
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
rt("/")(egg_index)
rt("/actions/product-collected", methods=["POST"])(product_collected)
def _render_error_form(locations, selected_location_id, error_message): def _render_error_form(locations, selected_location_id, error_message):
"""Render form with error message. """Render form with error message.
@@ -181,6 +183,7 @@ def _render_error_form(locations, selected_location_id, error_message):
locations, locations,
selected_location_id=selected_location_id, selected_location_id=selected_location_id,
error=error_message, error=error_message,
action=product_collected,
), ),
title="Egg - AnimalTrack", title="Egg - AnimalTrack",
active_nav="egg", active_nav="egg",

View File

@@ -38,18 +38,9 @@ def get_feed_balance(db: Any, feed_type_code: str) -> int | None:
return row[0] if row else None return row[0] if row else None
def register_feed_routes(rt, app): def feed_index(request: Request):
"""Register feed capture routes.
Args:
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
@rt("/feed")
def feed_index(request: Request):
"""GET /feed - Feed Quick Capture page.""" """GET /feed - Feed Quick Capture page."""
db = app.state.db db = request.app.state.db
locations = LocationRepository(db).list_active() locations = LocationRepository(db).list_active()
feed_types = FeedTypeRepository(db).list_active() feed_types = FeedTypeRepository(db).list_active()
@@ -63,15 +54,17 @@ def register_feed_routes(rt, app):
locations, locations,
feed_types, feed_types,
active_tab=active_tab, active_tab=active_tab,
give_action=feed_given,
purchase_action=feed_purchased,
), ),
title="Feed - AnimalTrack", title="Feed - AnimalTrack",
active_nav="feed", active_nav="feed",
) )
@rt("/actions/feed-given", methods=["POST"])
async def feed_given(request: Request): async def feed_given(request: Request):
"""POST /actions/feed-given - Record feed given.""" """POST /actions/feed-given - Record feed given."""
db = app.state.db db = request.app.state.db
form = await request.form() form = await request.form()
# Extract form data # Extract form data
@@ -191,6 +184,8 @@ def register_feed_routes(rt, app):
selected_feed_type_code=feed_type_code, selected_feed_type_code=feed_type_code,
default_amount_kg=default_amount_kg, default_amount_kg=default_amount_kg,
balance_warning=balance_warning, balance_warning=balance_warning,
give_action=feed_given,
purchase_action=feed_purchased,
), ),
title="Feed - AnimalTrack", title="Feed - AnimalTrack",
active_nav="feed", active_nav="feed",
@@ -210,10 +205,10 @@ def register_feed_routes(rt, app):
return response return response
@rt("/actions/feed-purchased", methods=["POST"])
async def feed_purchased(request: Request): async def feed_purchased(request: Request):
"""POST /actions/feed-purchased - Record feed purchase.""" """POST /actions/feed-purchased - Record feed purchase."""
db = app.state.db db = request.app.state.db
form = await request.form() form = await request.form()
# Extract form data # Extract form data
@@ -339,6 +334,8 @@ def register_feed_routes(rt, app):
locations, locations,
feed_types, feed_types,
active_tab="purchase", active_tab="purchase",
give_action=feed_given,
purchase_action=feed_purchased,
), ),
title="Feed - AnimalTrack", title="Feed - AnimalTrack",
active_nav="feed", active_nav="feed",
@@ -359,6 +356,18 @@ def register_feed_routes(rt, app):
return response return response
def register_feed_routes(rt, app):
"""Register feed capture routes.
Args:
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
rt("/feed")(feed_index)
rt("/actions/feed-given", methods=["POST"])(feed_given)
rt("/actions/feed-purchased", methods=["POST"])(feed_purchased)
def _render_give_error( def _render_give_error(
locations, locations,
feed_types, feed_types,
@@ -388,6 +397,8 @@ def _render_give_error(
selected_location_id=selected_location_id, selected_location_id=selected_location_id,
selected_feed_type_code=selected_feed_type_code, selected_feed_type_code=selected_feed_type_code,
give_error=error_message, give_error=error_message,
give_action=feed_given,
purchase_action=feed_purchased,
), ),
title="Feed - AnimalTrack", title="Feed - AnimalTrack",
active_nav="feed", active_nav="feed",
@@ -416,6 +427,8 @@ def _render_purchase_error(locations, feed_types, error_message):
feed_types, feed_types,
active_tab="purchase", active_tab="purchase",
purchase_error=error_message, purchase_error=error_message,
give_action=feed_given,
purchase_action=feed_purchased,
), ),
title="Feed - AnimalTrack", title="Feed - AnimalTrack",
active_nav="feed", active_nav="feed",

View File

@@ -1,6 +1,9 @@
# ABOUTME: Templates for Egg Quick Capture form. # ABOUTME: Templates for Egg Quick Capture form.
# ABOUTME: Provides form components for recording egg collections. # ABOUTME: Provides form components for recording egg collections.
from collections.abc import Callable
from typing import Any
from fasthtml.common import H2, Form, Hidden, Option from fasthtml.common import H2, Form, Hidden, Option
from monsterui.all import Button, ButtonT, LabelInput, LabelSelect, LabelTextArea from monsterui.all import Button, ButtonT, LabelInput, LabelSelect, LabelTextArea
from ulid import ULID from ulid import ULID
@@ -12,6 +15,7 @@ def egg_form(
locations: list[Location], locations: list[Location],
selected_location_id: str | None = None, selected_location_id: str | None = None,
error: str | None = None, error: str | None = None,
action: Callable[..., Any] | str = "/actions/product-collected",
) -> Form: ) -> Form:
"""Create the Egg Quick Capture form. """Create the Egg Quick Capture form.
@@ -19,6 +23,7 @@ def egg_form(
locations: List of active locations for the dropdown. locations: List of active locations for the dropdown.
selected_location_id: Pre-selected location ID (sticks after submission). selected_location_id: Pre-selected location ID (sticks after submission).
error: Optional error message to display. error: Optional error message to display.
action: Route function or URL string for form submission.
Returns: Returns:
Form component for egg collection. Form component for egg collection.
@@ -82,7 +87,7 @@ def egg_form(
# Submit button # Submit button
Button("Record Eggs", type="submit", cls=ButtonT.primary), Button("Record Eggs", type="submit", cls=ButtonT.primary),
# Form submission via standard action/method (hx-boost handles AJAX) # Form submission via standard action/method (hx-boost handles AJAX)
action="/actions/product-collected", action=action,
method="post", method="post",
cls="space-y-4", cls="space-y-4",
) )

View File

@@ -1,6 +1,9 @@
# ABOUTME: Templates for Feed Quick Capture forms. # ABOUTME: Templates for Feed Quick Capture forms.
# ABOUTME: Provides form components for recording feed given and purchases. # ABOUTME: Provides form components for recording feed given and purchases.
from collections.abc import Callable
from typing import Any
from fasthtml.common import H1, H2, A, Div, Form, Hidden, Li, Option, P, Ul from fasthtml.common import H1, H2, A, Div, Form, Hidden, Li, Option, P, Ul
from monsterui.all import ( from monsterui.all import (
Button, Button,
@@ -25,6 +28,8 @@ def feed_page(
give_error: str | None = None, give_error: str | None = None,
purchase_error: str | None = None, purchase_error: str | None = None,
balance_warning: str | None = None, balance_warning: str | None = None,
give_action: Callable[..., Any] | str = "/actions/feed-given",
purchase_action: Callable[..., Any] | str = "/actions/feed-purchased",
): ):
"""Create the Feed Quick Capture page with tabbed forms. """Create the Feed Quick Capture page with tabbed forms.
@@ -38,6 +43,8 @@ def feed_page(
give_error: Error message for give form. give_error: Error message for give form.
purchase_error: Error message for purchase form. purchase_error: Error message for purchase form.
balance_warning: Warning about negative inventory balance. balance_warning: Warning about negative inventory balance.
give_action: Route function or URL for give feed form.
purchase_action: Route function or URL for purchase feed form.
Returns: Returns:
Page content with tabbed forms. Page content with tabbed forms.
@@ -76,11 +83,12 @@ def feed_page(
default_amount_kg=default_amount_kg, default_amount_kg=default_amount_kg,
error=give_error, error=give_error,
balance_warning=balance_warning, balance_warning=balance_warning,
action=give_action,
), ),
cls="uk-active" if give_active else "", cls="uk-active" if give_active else "",
), ),
Li( Li(
purchase_feed_form(feed_types, error=purchase_error), purchase_feed_form(feed_types, error=purchase_error, action=purchase_action),
cls="" if give_active else "uk-active", cls="" if give_active else "uk-active",
), ),
), ),
@@ -96,6 +104,7 @@ def give_feed_form(
default_amount_kg: int | None = None, default_amount_kg: int | None = None,
error: str | None = None, error: str | None = None,
balance_warning: str | None = None, balance_warning: str | None = None,
action: Callable[..., Any] | str = "/actions/feed-given",
) -> Form: ) -> Form:
"""Create the Give Feed form. """Create the Give Feed form.
@@ -107,6 +116,7 @@ def give_feed_form(
default_amount_kg: Default value for amount field. default_amount_kg: Default value for amount field.
error: Error message to display. error: Error message to display.
balance_warning: Warning about negative balance. balance_warning: Warning about negative balance.
action: Route function or URL for form submission.
Returns: Returns:
Form component for giving feed. Form component for giving feed.
@@ -196,7 +206,7 @@ def give_feed_form(
Hidden(name="nonce", value=str(ULID())), Hidden(name="nonce", value=str(ULID())),
# Submit button # Submit button
Button("Record Feed Given", type="submit", cls=ButtonT.primary), Button("Record Feed Given", type="submit", cls=ButtonT.primary),
action="/actions/feed-given", action=action,
method="post", method="post",
cls="space-y-4", cls="space-y-4",
) )
@@ -205,12 +215,14 @@ def give_feed_form(
def purchase_feed_form( def purchase_feed_form(
feed_types: list[FeedType], feed_types: list[FeedType],
error: str | None = None, error: str | None = None,
action: Callable[..., Any] | str = "/actions/feed-purchased",
) -> Form: ) -> Form:
"""Create the Purchase Feed form. """Create the Purchase Feed form.
Args: Args:
feed_types: List of active feed types. feed_types: List of active feed types.
error: Error message to display. error: Error message to display.
action: Route function or URL for form submission.
Returns: Returns:
Form component for purchasing feed. Form component for purchasing feed.
@@ -290,7 +302,7 @@ def purchase_feed_form(
Hidden(name="nonce", value=str(ULID())), Hidden(name="nonce", value=str(ULID())),
# Submit button # Submit button
Button("Record Purchase", type="submit", cls=ButtonT.primary), Button("Record Purchase", type="submit", cls=ButtonT.primary),
action="/actions/feed-purchased", action=action,
method="post", method="post",
cls="space-y-4", cls="space-y-4",
) )