From b1bfdfb05c8aa4c73ea1dde66579c7f104afa99b Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Tue, 30 Dec 2025 11:11:08 +0000 Subject: [PATCH] refactor: move route handlers to module level for idiomatic FastHTML MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/animaltrack/web/routes/eggs.py | 213 ++++----- src/animaltrack/web/routes/feed.py | 637 +++++++++++++------------- src/animaltrack/web/templates/eggs.py | 7 +- src/animaltrack/web/templates/feed.py | 18 +- 4 files changed, 454 insertions(+), 421 deletions(-) diff --git a/src/animaltrack/web/routes/eggs.py b/src/animaltrack/web/routes/eggs.py index 3e85af7..8b9dec5 100644 --- a/src/animaltrack/web/routes/eggs.py +++ b/src/animaltrack/web/routes/eggs.py @@ -49,6 +49,111 @@ def resolve_ducks_at_location(db: Any, location_id: str, ts_utc: int) -> list[st return [row[0] for row in rows] +def egg_index(request: Request): + """GET / - Egg Quick Capture form.""" + db = request.app.state.db + locations = LocationRepository(db).list_active() + + # Check for pre-selected location from query params + selected_location_id = request.query_params.get("location_id") + + return page( + egg_form(locations, selected_location_id=selected_location_id, action=product_collected), + title="Egg - AnimalTrack", + active_nav="egg", + ) + + +async def product_collected(request: Request): + """POST /actions/product-collected - Record egg collection.""" + db = request.app.state.db + form = await request.form() + + # Extract form data + location_id = form.get("location_id", "") + quantity_str = form.get("quantity", "0") + notes = form.get("notes") or None + nonce = form.get("nonce") + + # Get locations for potential re-render + locations = LocationRepository(db).list_active() + + # Validate location_id + if not location_id: + return _render_error_form(locations, None, "Please select a location") + + # Validate quantity + try: + quantity = int(quantity_str) + except ValueError: + return _render_error_form(locations, location_id, "Quantity must be a number") + + if quantity < 1: + return _render_error_form(locations, location_id, "Quantity must be at least 1") + + # Get current timestamp + ts_utc = int(time.time() * 1000) + + # Resolve ducks at location + resolved_ids = resolve_ducks_at_location(db, location_id, ts_utc) + + if not resolved_ids: + return _render_error_form(locations, location_id, "No ducks at this location") + + # Create product service + event_store = EventStore(db) + registry = ProjectionRegistry() + registry.register(AnimalRegistryProjection(db)) + registry.register(EventAnimalsProjection(db)) + registry.register(IntervalProjection(db)) + registry.register(ProductsProjection(db)) + + product_service = ProductService(db, event_store, registry) + + # Create payload + payload = ProductCollectedPayload( + location_id=location_id, + product_code="egg.duck", + quantity=quantity, + resolved_ids=resolved_ids, + notes=notes, + ) + + # Get actor from auth + auth = request.scope.get("auth") + actor = auth.username if auth else "unknown" + + # Collect product + try: + product_service.collect_product( + payload=payload, + ts_utc=ts_utc, + actor=actor, + nonce=nonce, + route="/actions/product-collected", + ) + except ValidationError as e: + return _render_error_form(locations, location_id, str(e)) + + # Success: re-render form with location sticking, qty cleared + response = HTMLResponse( + content=str( + page( + egg_form(locations, selected_location_id=location_id, action=product_collected), + title="Egg - AnimalTrack", + active_nav="egg", + ) + ), + ) + + # Add toast trigger header + response.headers["HX-Trigger"] = json.dumps( + {"showToast": {"message": f"Recorded {quantity} eggs", "type": "success"}} + ) + + return response + + def register_egg_routes(rt, app): """Register egg capture routes. @@ -56,111 +161,8 @@ def register_egg_routes(rt, app): rt: FastHTML route decorator. app: FastHTML application instance. """ - - @rt("/") - def index(request: Request): - """GET / - Egg Quick Capture form.""" - db = app.state.db - locations = LocationRepository(db).list_active() - - # Check for pre-selected location from query params - selected_location_id = request.query_params.get("location_id") - - return page( - egg_form(locations, selected_location_id=selected_location_id), - title="Egg - AnimalTrack", - active_nav="egg", - ) - - @rt("/actions/product-collected", methods=["POST"]) - async def product_collected(request: Request): - """POST /actions/product-collected - Record egg collection.""" - db = app.state.db - form = await request.form() - - # Extract form data - location_id = form.get("location_id", "") - quantity_str = form.get("quantity", "0") - notes = form.get("notes") or None - nonce = form.get("nonce") - - # Get locations for potential re-render - locations = LocationRepository(db).list_active() - - # Validate location_id - if not location_id: - return _render_error_form(locations, None, "Please select a location") - - # Validate quantity - try: - quantity = int(quantity_str) - except ValueError: - return _render_error_form(locations, location_id, "Quantity must be a number") - - if quantity < 1: - return _render_error_form(locations, location_id, "Quantity must be at least 1") - - # Get current timestamp - ts_utc = int(time.time() * 1000) - - # Resolve ducks at location - resolved_ids = resolve_ducks_at_location(db, location_id, ts_utc) - - if not resolved_ids: - return _render_error_form(locations, location_id, "No ducks at this location") - - # Create product service - event_store = EventStore(db) - registry = ProjectionRegistry() - registry.register(AnimalRegistryProjection(db)) - registry.register(EventAnimalsProjection(db)) - registry.register(IntervalProjection(db)) - registry.register(ProductsProjection(db)) - - product_service = ProductService(db, event_store, registry) - - # Create payload - payload = ProductCollectedPayload( - location_id=location_id, - product_code="egg.duck", - quantity=quantity, - resolved_ids=resolved_ids, - notes=notes, - ) - - # Get actor from auth - auth = request.scope.get("auth") - actor = auth.username if auth else "unknown" - - # Collect product - try: - product_service.collect_product( - payload=payload, - ts_utc=ts_utc, - actor=actor, - nonce=nonce, - route="/actions/product-collected", - ) - except ValidationError as e: - return _render_error_form(locations, location_id, str(e)) - - # Success: re-render form with location sticking, qty cleared - response = HTMLResponse( - content=str( - page( - egg_form(locations, selected_location_id=location_id), - title="Egg - AnimalTrack", - active_nav="egg", - ) - ), - ) - - # Add toast trigger header - response.headers["HX-Trigger"] = json.dumps( - {"showToast": {"message": f"Recorded {quantity} eggs", "type": "success"}} - ) - - return response + rt("/")(egg_index) + rt("/actions/product-collected", methods=["POST"])(product_collected) def _render_error_form(locations, selected_location_id, error_message): @@ -181,6 +183,7 @@ def _render_error_form(locations, selected_location_id, error_message): locations, selected_location_id=selected_location_id, error=error_message, + action=product_collected, ), title="Egg - AnimalTrack", active_nav="egg", diff --git a/src/animaltrack/web/routes/feed.py b/src/animaltrack/web/routes/feed.py index 3dcbc5f..5387cf8 100644 --- a/src/animaltrack/web/routes/feed.py +++ b/src/animaltrack/web/routes/feed.py @@ -38,6 +38,324 @@ def get_feed_balance(db: Any, feed_type_code: str) -> int | None: return row[0] if row else None +def feed_index(request: Request): + """GET /feed - Feed Quick Capture page.""" + db = request.app.state.db + locations = LocationRepository(db).list_active() + feed_types = FeedTypeRepository(db).list_active() + + # Check for active tab from query params + active_tab = request.query_params.get("tab", "give") + if active_tab not in ("give", "purchase"): + active_tab = "give" + + return page( + feed_page( + locations, + feed_types, + active_tab=active_tab, + give_action=feed_given, + purchase_action=feed_purchased, + ), + title="Feed - AnimalTrack", + active_nav="feed", + ) + + +async def feed_given(request: Request): + """POST /actions/feed-given - Record feed given.""" + db = request.app.state.db + form = await request.form() + + # Extract form data + location_id = form.get("location_id", "") + feed_type_code = form.get("feed_type_code", "") + amount_kg_str = form.get("amount_kg", "0") + notes = form.get("notes") or None + nonce = form.get("nonce") + + # Get data for potential re-render + locations = LocationRepository(db).list_active() + feed_types = FeedTypeRepository(db).list_active() + + # Find default amount from feed type + default_amount_kg = 20 + for ft in feed_types: + if ft.code == feed_type_code: + default_amount_kg = ft.default_bag_size_kg + break + + # Validate location_id + if not location_id: + return _render_give_error( + locations, + feed_types, + "Please select a location", + selected_location_id=None, + selected_feed_type_code=feed_type_code or None, + ) + + # Validate feed_type_code + if not feed_type_code: + return _render_give_error( + locations, + feed_types, + "Please select a feed type", + selected_location_id=location_id, + selected_feed_type_code=None, + ) + + # Validate amount_kg + try: + amount_kg = int(amount_kg_str) + except ValueError: + return _render_give_error( + locations, + feed_types, + "Amount must be a number", + selected_location_id=location_id, + selected_feed_type_code=feed_type_code, + ) + + if amount_kg < 1: + return _render_give_error( + locations, + feed_types, + "Amount must be at least 1 kg", + selected_location_id=location_id, + selected_feed_type_code=feed_type_code, + ) + + # Get current timestamp + ts_utc = int(time.time() * 1000) + + # Create feed service + event_store = EventStore(db) + registry = ProjectionRegistry() + registry.register(FeedInventoryProjection(db)) + + feed_service = FeedService(db, event_store, registry) + + # Create payload + payload = FeedGivenPayload( + location_id=location_id, + feed_type_code=feed_type_code, + amount_kg=amount_kg, + notes=notes, + ) + + # Get actor from auth + auth = request.scope.get("auth") + actor = auth.username if auth else "unknown" + + # Give feed + try: + feed_service.give_feed( + payload=payload, + ts_utc=ts_utc, + actor=actor, + nonce=nonce, + route="/actions/feed-given", + ) + except ValidationError as e: + return _render_give_error( + locations, + feed_types, + str(e), + selected_location_id=location_id, + selected_feed_type_code=feed_type_code, + ) + + # Check for negative balance warning + balance = get_feed_balance(db, feed_type_code) + balance_warning = None + if balance is not None and balance < 0: + balance_warning = f"Warning: Inventory balance is now negative ({balance} kg)" + + # Success: re-render form with location/type sticking, amount reset + response = HTMLResponse( + content=str( + page( + feed_page( + locations, + feed_types, + active_tab="give", + selected_location_id=location_id, + selected_feed_type_code=feed_type_code, + default_amount_kg=default_amount_kg, + balance_warning=balance_warning, + give_action=feed_given, + purchase_action=feed_purchased, + ), + title="Feed - AnimalTrack", + active_nav="feed", + ) + ), + ) + + # Add toast trigger header + response.headers["HX-Trigger"] = json.dumps( + { + "showToast": { + "message": f"Recorded {amount_kg}kg {feed_type_code}", + "type": "success", + } + } + ) + + return response + + +async def feed_purchased(request: Request): + """POST /actions/feed-purchased - Record feed purchase.""" + db = request.app.state.db + form = await request.form() + + # Extract form data + feed_type_code = form.get("feed_type_code", "") + bag_size_kg_str = form.get("bag_size_kg", "0") + bags_count_str = form.get("bags_count", "0") + bag_price_cents_str = form.get("bag_price_cents", "0") + vendor = form.get("vendor") or None + notes = form.get("notes") or None + nonce = form.get("nonce") + + # Get data for potential re-render + locations = LocationRepository(db).list_active() + feed_types = FeedTypeRepository(db).list_active() + + # Validate feed_type_code + if not feed_type_code: + return _render_purchase_error( + locations, + feed_types, + "Please select a feed type", + ) + + # Validate bag_size_kg + try: + bag_size_kg = int(bag_size_kg_str) + except ValueError: + return _render_purchase_error( + locations, + feed_types, + "Bag size must be a number", + ) + + if bag_size_kg < 1: + return _render_purchase_error( + locations, + feed_types, + "Bag size must be at least 1 kg", + ) + + # Validate bags_count + try: + bags_count = int(bags_count_str) + except ValueError: + return _render_purchase_error( + locations, + feed_types, + "Bags count must be a number", + ) + + if bags_count < 1: + return _render_purchase_error( + locations, + feed_types, + "Bags count must be at least 1", + ) + + # Validate bag_price_cents + try: + bag_price_cents = int(bag_price_cents_str) + except ValueError: + return _render_purchase_error( + locations, + feed_types, + "Price must be a number", + ) + + if bag_price_cents < 0: + return _render_purchase_error( + locations, + feed_types, + "Price cannot be negative", + ) + + # Get current timestamp + ts_utc = int(time.time() * 1000) + + # Create feed service + event_store = EventStore(db) + registry = ProjectionRegistry() + registry.register(FeedInventoryProjection(db)) + + feed_service = FeedService(db, event_store, registry) + + # Create payload + payload = FeedPurchasedPayload( + feed_type_code=feed_type_code, + bag_size_kg=bag_size_kg, + bags_count=bags_count, + bag_price_cents=bag_price_cents, + vendor=vendor, + notes=notes, + ) + + # Get actor from auth + auth = request.scope.get("auth") + actor = auth.username if auth else "unknown" + + # Purchase feed + try: + feed_service.purchase_feed( + payload=payload, + ts_utc=ts_utc, + actor=actor, + nonce=nonce, + route="/actions/feed-purchased", + ) + except ValidationError as e: + return _render_purchase_error( + locations, + feed_types, + str(e), + ) + + # Calculate total for toast + total_kg = bag_size_kg * bags_count + + # Success: re-render form with fields cleared + response = HTMLResponse( + content=str( + page( + feed_page( + locations, + feed_types, + active_tab="purchase", + give_action=feed_given, + purchase_action=feed_purchased, + ), + title="Feed - AnimalTrack", + active_nav="feed", + ) + ), + ) + + # Add toast trigger header + response.headers["HX-Trigger"] = json.dumps( + { + "showToast": { + "message": f"Purchased {total_kg}kg {feed_type_code}", + "type": "success", + } + } + ) + + return response + + def register_feed_routes(rt, app): """Register feed capture routes. @@ -45,318 +363,9 @@ def register_feed_routes(rt, app): rt: FastHTML route decorator. app: FastHTML application instance. """ - - @rt("/feed") - def feed_index(request: Request): - """GET /feed - Feed Quick Capture page.""" - db = app.state.db - locations = LocationRepository(db).list_active() - feed_types = FeedTypeRepository(db).list_active() - - # Check for active tab from query params - active_tab = request.query_params.get("tab", "give") - if active_tab not in ("give", "purchase"): - active_tab = "give" - - return page( - feed_page( - locations, - feed_types, - active_tab=active_tab, - ), - title="Feed - AnimalTrack", - active_nav="feed", - ) - - @rt("/actions/feed-given", methods=["POST"]) - async def feed_given(request: Request): - """POST /actions/feed-given - Record feed given.""" - db = app.state.db - form = await request.form() - - # Extract form data - location_id = form.get("location_id", "") - feed_type_code = form.get("feed_type_code", "") - amount_kg_str = form.get("amount_kg", "0") - notes = form.get("notes") or None - nonce = form.get("nonce") - - # Get data for potential re-render - locations = LocationRepository(db).list_active() - feed_types = FeedTypeRepository(db).list_active() - - # Find default amount from feed type - default_amount_kg = 20 - for ft in feed_types: - if ft.code == feed_type_code: - default_amount_kg = ft.default_bag_size_kg - break - - # Validate location_id - if not location_id: - return _render_give_error( - locations, - feed_types, - "Please select a location", - selected_location_id=None, - selected_feed_type_code=feed_type_code or None, - ) - - # Validate feed_type_code - if not feed_type_code: - return _render_give_error( - locations, - feed_types, - "Please select a feed type", - selected_location_id=location_id, - selected_feed_type_code=None, - ) - - # Validate amount_kg - try: - amount_kg = int(amount_kg_str) - except ValueError: - return _render_give_error( - locations, - feed_types, - "Amount must be a number", - selected_location_id=location_id, - selected_feed_type_code=feed_type_code, - ) - - if amount_kg < 1: - return _render_give_error( - locations, - feed_types, - "Amount must be at least 1 kg", - selected_location_id=location_id, - selected_feed_type_code=feed_type_code, - ) - - # Get current timestamp - ts_utc = int(time.time() * 1000) - - # Create feed service - event_store = EventStore(db) - registry = ProjectionRegistry() - registry.register(FeedInventoryProjection(db)) - - feed_service = FeedService(db, event_store, registry) - - # Create payload - payload = FeedGivenPayload( - location_id=location_id, - feed_type_code=feed_type_code, - amount_kg=amount_kg, - notes=notes, - ) - - # Get actor from auth - auth = request.scope.get("auth") - actor = auth.username if auth else "unknown" - - # Give feed - try: - feed_service.give_feed( - payload=payload, - ts_utc=ts_utc, - actor=actor, - nonce=nonce, - route="/actions/feed-given", - ) - except ValidationError as e: - return _render_give_error( - locations, - feed_types, - str(e), - selected_location_id=location_id, - selected_feed_type_code=feed_type_code, - ) - - # Check for negative balance warning - balance = get_feed_balance(db, feed_type_code) - balance_warning = None - if balance is not None and balance < 0: - balance_warning = f"Warning: Inventory balance is now negative ({balance} kg)" - - # Success: re-render form with location/type sticking, amount reset - response = HTMLResponse( - content=str( - page( - feed_page( - locations, - feed_types, - active_tab="give", - selected_location_id=location_id, - selected_feed_type_code=feed_type_code, - default_amount_kg=default_amount_kg, - balance_warning=balance_warning, - ), - title="Feed - AnimalTrack", - active_nav="feed", - ) - ), - ) - - # Add toast trigger header - response.headers["HX-Trigger"] = json.dumps( - { - "showToast": { - "message": f"Recorded {amount_kg}kg {feed_type_code}", - "type": "success", - } - } - ) - - return response - - @rt("/actions/feed-purchased", methods=["POST"]) - async def feed_purchased(request: Request): - """POST /actions/feed-purchased - Record feed purchase.""" - db = app.state.db - form = await request.form() - - # Extract form data - feed_type_code = form.get("feed_type_code", "") - bag_size_kg_str = form.get("bag_size_kg", "0") - bags_count_str = form.get("bags_count", "0") - bag_price_cents_str = form.get("bag_price_cents", "0") - vendor = form.get("vendor") or None - notes = form.get("notes") or None - nonce = form.get("nonce") - - # Get data for potential re-render - locations = LocationRepository(db).list_active() - feed_types = FeedTypeRepository(db).list_active() - - # Validate feed_type_code - if not feed_type_code: - return _render_purchase_error( - locations, - feed_types, - "Please select a feed type", - ) - - # Validate bag_size_kg - try: - bag_size_kg = int(bag_size_kg_str) - except ValueError: - return _render_purchase_error( - locations, - feed_types, - "Bag size must be a number", - ) - - if bag_size_kg < 1: - return _render_purchase_error( - locations, - feed_types, - "Bag size must be at least 1 kg", - ) - - # Validate bags_count - try: - bags_count = int(bags_count_str) - except ValueError: - return _render_purchase_error( - locations, - feed_types, - "Bags count must be a number", - ) - - if bags_count < 1: - return _render_purchase_error( - locations, - feed_types, - "Bags count must be at least 1", - ) - - # Validate bag_price_cents - try: - bag_price_cents = int(bag_price_cents_str) - except ValueError: - return _render_purchase_error( - locations, - feed_types, - "Price must be a number", - ) - - if bag_price_cents < 0: - return _render_purchase_error( - locations, - feed_types, - "Price cannot be negative", - ) - - # Get current timestamp - ts_utc = int(time.time() * 1000) - - # Create feed service - event_store = EventStore(db) - registry = ProjectionRegistry() - registry.register(FeedInventoryProjection(db)) - - feed_service = FeedService(db, event_store, registry) - - # Create payload - payload = FeedPurchasedPayload( - feed_type_code=feed_type_code, - bag_size_kg=bag_size_kg, - bags_count=bags_count, - bag_price_cents=bag_price_cents, - vendor=vendor, - notes=notes, - ) - - # Get actor from auth - auth = request.scope.get("auth") - actor = auth.username if auth else "unknown" - - # Purchase feed - try: - feed_service.purchase_feed( - payload=payload, - ts_utc=ts_utc, - actor=actor, - nonce=nonce, - route="/actions/feed-purchased", - ) - except ValidationError as e: - return _render_purchase_error( - locations, - feed_types, - str(e), - ) - - # Calculate total for toast - total_kg = bag_size_kg * bags_count - - # Success: re-render form with fields cleared - response = HTMLResponse( - content=str( - page( - feed_page( - locations, - feed_types, - active_tab="purchase", - ), - title="Feed - AnimalTrack", - active_nav="feed", - ) - ), - ) - - # Add toast trigger header - response.headers["HX-Trigger"] = json.dumps( - { - "showToast": { - "message": f"Purchased {total_kg}kg {feed_type_code}", - "type": "success", - } - } - ) - - return response + rt("/feed")(feed_index) + rt("/actions/feed-given", methods=["POST"])(feed_given) + rt("/actions/feed-purchased", methods=["POST"])(feed_purchased) def _render_give_error( @@ -388,6 +397,8 @@ def _render_give_error( selected_location_id=selected_location_id, selected_feed_type_code=selected_feed_type_code, give_error=error_message, + give_action=feed_given, + purchase_action=feed_purchased, ), title="Feed - AnimalTrack", active_nav="feed", @@ -416,6 +427,8 @@ def _render_purchase_error(locations, feed_types, error_message): feed_types, active_tab="purchase", purchase_error=error_message, + give_action=feed_given, + purchase_action=feed_purchased, ), title="Feed - AnimalTrack", active_nav="feed", diff --git a/src/animaltrack/web/templates/eggs.py b/src/animaltrack/web/templates/eggs.py index 117d8c7..c4466ba 100644 --- a/src/animaltrack/web/templates/eggs.py +++ b/src/animaltrack/web/templates/eggs.py @@ -1,6 +1,9 @@ # ABOUTME: Templates for Egg Quick Capture form. # 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 monsterui.all import Button, ButtonT, LabelInput, LabelSelect, LabelTextArea from ulid import ULID @@ -12,6 +15,7 @@ def egg_form( locations: list[Location], selected_location_id: str | None = None, error: str | None = None, + action: Callable[..., Any] | str = "/actions/product-collected", ) -> Form: """Create the Egg Quick Capture form. @@ -19,6 +23,7 @@ def egg_form( locations: List of active locations for the dropdown. selected_location_id: Pre-selected location ID (sticks after submission). error: Optional error message to display. + action: Route function or URL string for form submission. Returns: Form component for egg collection. @@ -82,7 +87,7 @@ def egg_form( # Submit button Button("Record Eggs", type="submit", cls=ButtonT.primary), # Form submission via standard action/method (hx-boost handles AJAX) - action="/actions/product-collected", + action=action, method="post", cls="space-y-4", ) diff --git a/src/animaltrack/web/templates/feed.py b/src/animaltrack/web/templates/feed.py index 63390a0..ae9084f 100644 --- a/src/animaltrack/web/templates/feed.py +++ b/src/animaltrack/web/templates/feed.py @@ -1,6 +1,9 @@ # ABOUTME: Templates for Feed Quick Capture forms. # 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 monsterui.all import ( Button, @@ -25,6 +28,8 @@ def feed_page( give_error: str | None = None, purchase_error: 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. @@ -38,6 +43,8 @@ def feed_page( give_error: Error message for give form. purchase_error: Error message for purchase form. 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: Page content with tabbed forms. @@ -76,11 +83,12 @@ def feed_page( default_amount_kg=default_amount_kg, error=give_error, balance_warning=balance_warning, + action=give_action, ), cls="uk-active" if give_active else "", ), 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", ), ), @@ -96,6 +104,7 @@ def give_feed_form( default_amount_kg: int | None = None, error: str | None = None, balance_warning: str | None = None, + action: Callable[..., Any] | str = "/actions/feed-given", ) -> Form: """Create the Give Feed form. @@ -107,6 +116,7 @@ def give_feed_form( default_amount_kg: Default value for amount field. error: Error message to display. balance_warning: Warning about negative balance. + action: Route function or URL for form submission. Returns: Form component for giving feed. @@ -196,7 +206,7 @@ def give_feed_form( Hidden(name="nonce", value=str(ULID())), # Submit button Button("Record Feed Given", type="submit", cls=ButtonT.primary), - action="/actions/feed-given", + action=action, method="post", cls="space-y-4", ) @@ -205,12 +215,14 @@ def give_feed_form( def purchase_feed_form( feed_types: list[FeedType], error: str | None = None, + action: Callable[..., Any] | str = "/actions/feed-purchased", ) -> Form: """Create the Purchase Feed form. Args: feed_types: List of active feed types. error: Error message to display. + action: Route function or URL for form submission. Returns: Form component for purchasing feed. @@ -290,7 +302,7 @@ def purchase_feed_form( Hidden(name="nonce", value=str(ULID())), # Submit button Button("Record Purchase", type="submit", cls=ButtonT.primary), - action="/actions/feed-purchased", + action=action, method="post", cls="space-y-4", )