Preserve form field values in eggs.py on validation errors

When a validation error occurs in the harvest or sell forms, the
entered field values are now preserved and redisplayed to the user.
This prevents the frustration of having to re-enter all values after
a single field fails validation.

Template changes (eggs.py):
- Added default_* parameters to harvest_form and sell_form
- Updated LabelInput/LabelTextArea fields to use these values

Route changes (routes/eggs.py):
- Updated _render_harvest_error to accept quantity and notes
- Updated _render_sell_error to accept quantity, total_price_cents,
  buyer, and notes
- All error return calls now pass form values through

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-09 12:10:54 +00:00
parent b09d3088eb
commit a87b5cbac6
2 changed files with 179 additions and 15 deletions

View File

@@ -365,7 +365,14 @@ async def product_collected(request: Request, session):
# Validate location_id # Validate location_id
if not location_id: if not location_id:
return _render_harvest_error( return _render_harvest_error(
request, db, locations, products, None, "Please select a location" request,
db,
locations,
products,
None,
"Please select a location",
quantity=quantity_str,
notes=notes,
) )
# Validate quantity # Validate quantity
@@ -373,12 +380,26 @@ async def product_collected(request: Request, session):
quantity = int(quantity_str) quantity = int(quantity_str)
except ValueError: except ValueError:
return _render_harvest_error( return _render_harvest_error(
request, db, locations, products, location_id, "Quantity must be a number" request,
db,
locations,
products,
location_id,
"Quantity must be a number",
quantity=quantity_str,
notes=notes,
) )
if quantity < 0: if quantity < 0:
return _render_harvest_error( return _render_harvest_error(
request, db, locations, products, location_id, "Quantity cannot be negative" request,
db,
locations,
products,
location_id,
"Quantity cannot be negative",
quantity=quantity_str,
notes=notes,
) )
# Get timestamp - use provided or current (supports backdating) # Get timestamp - use provided or current (supports backdating)
@@ -389,7 +410,14 @@ async def product_collected(request: Request, session):
if not resolved_ids: if not resolved_ids:
return _render_harvest_error( return _render_harvest_error(
request, db, locations, products, location_id, "No ducks at this location" request,
db,
locations,
products,
location_id,
"No ducks at this location",
quantity=quantity_str,
notes=notes,
) )
# Create product service # Create product service
@@ -422,7 +450,16 @@ async def product_collected(request: Request, session):
route="/actions/product-collected", route="/actions/product-collected",
) )
except ValidationError as e: except ValidationError as e:
return _render_harvest_error(request, db, locations, products, location_id, str(e)) return _render_harvest_error(
request,
db,
locations,
products,
location_id,
str(e),
quantity=quantity_str,
notes=notes,
)
# Save user defaults (only if user exists in database) # Save user defaults (only if user exists in database)
if UserRepository(db).get(actor): if UserRepository(db).get(actor):
@@ -486,19 +523,48 @@ async def product_sold(request: Request, session):
# Validate product_code # Validate product_code
if not product_code: if not product_code:
return _render_sell_error(request, db, locations, products, None, "Please select a product") return _render_sell_error(
request,
db,
locations,
products,
None,
"Please select a product",
quantity=quantity_str,
total_price_cents=total_price_str,
buyer=buyer,
notes=notes,
)
# Validate quantity # Validate quantity
try: try:
quantity = int(quantity_str) quantity = int(quantity_str)
except ValueError: except ValueError:
return _render_sell_error( return _render_sell_error(
request, db, locations, products, product_code, "Quantity must be a number" request,
db,
locations,
products,
product_code,
"Quantity must be a number",
quantity=quantity_str,
total_price_cents=total_price_str,
buyer=buyer,
notes=notes,
) )
if quantity < 1: if quantity < 1:
return _render_sell_error( return _render_sell_error(
request, db, locations, products, product_code, "Quantity must be at least 1" request,
db,
locations,
products,
product_code,
"Quantity must be at least 1",
quantity=quantity_str,
total_price_cents=total_price_str,
buyer=buyer,
notes=notes,
) )
# Validate total_price_cents # Validate total_price_cents
@@ -506,12 +572,30 @@ async def product_sold(request: Request, session):
total_price_cents = int(total_price_str) total_price_cents = int(total_price_str)
except ValueError: except ValueError:
return _render_sell_error( return _render_sell_error(
request, db, locations, products, product_code, "Total price must be a number" request,
db,
locations,
products,
product_code,
"Total price must be a number",
quantity=quantity_str,
total_price_cents=total_price_str,
buyer=buyer,
notes=notes,
) )
if total_price_cents < 0: if total_price_cents < 0:
return _render_sell_error( return _render_sell_error(
request, db, locations, products, product_code, "Total price cannot be negative" request,
db,
locations,
products,
product_code,
"Total price cannot be negative",
quantity=quantity_str,
total_price_cents=total_price_str,
buyer=buyer,
notes=notes,
) )
# Get timestamp - use provided or current (supports backdating) # Get timestamp - use provided or current (supports backdating)
@@ -544,7 +628,18 @@ async def product_sold(request: Request, session):
route="/actions/product-sold", route="/actions/product-sold",
) )
except ValidationError as e: except ValidationError as e:
return _render_sell_error(request, db, locations, products, product_code, str(e)) return _render_sell_error(
request,
db,
locations,
products,
product_code,
str(e),
quantity=quantity_str,
total_price_cents=total_price_str,
buyer=buyer,
notes=notes,
)
# Add success toast with link to event # Add success toast with link to event
add_toast( add_toast(
@@ -573,8 +668,17 @@ async def product_sold(request: Request, session):
) )
def _render_harvest_error(request, db, locations, products, selected_location_id, error_message): def _render_harvest_error(
"""Render harvest form with error message. request,
db,
locations,
products,
selected_location_id,
error_message,
quantity: str | None = None,
notes: str | None = None,
):
"""Render harvest form with error message and preserved field values.
Args: Args:
request: The HTTP request. request: The HTTP request.
@@ -583,6 +687,8 @@ def _render_harvest_error(request, db, locations, products, selected_location_id
products: List of sellable products. products: List of sellable products.
selected_location_id: Currently selected location. selected_location_id: Currently selected location.
error_message: Error message to display. error_message: Error message to display.
quantity: Quantity value to preserve.
notes: Notes value to preserve.
Returns: Returns:
HTMLResponse with 422 status. HTMLResponse with 422 status.
@@ -600,6 +706,8 @@ def _render_harvest_error(request, db, locations, products, selected_location_id
harvest_error=error_message, harvest_error=error_message,
harvest_action=product_collected, harvest_action=product_collected,
sell_action=product_sold, sell_action=product_sold,
harvest_quantity=quantity,
harvest_notes=notes,
**display_data, **display_data,
), ),
title="Eggs - AnimalTrack", title="Eggs - AnimalTrack",
@@ -610,8 +718,19 @@ def _render_harvest_error(request, db, locations, products, selected_location_id
) )
def _render_sell_error(request, db, locations, products, selected_product_code, error_message): def _render_sell_error(
"""Render sell form with error message. request,
db,
locations,
products,
selected_product_code,
error_message,
quantity: str | None = None,
total_price_cents: str | None = None,
buyer: str | None = None,
notes: str | None = None,
):
"""Render sell form with error message and preserved field values.
Args: Args:
request: The HTTP request. request: The HTTP request.
@@ -620,6 +739,10 @@ def _render_sell_error(request, db, locations, products, selected_product_code,
products: List of sellable products. products: List of sellable products.
selected_product_code: Currently selected product code. selected_product_code: Currently selected product code.
error_message: Error message to display. error_message: Error message to display.
quantity: Quantity value to preserve.
total_price_cents: Total price value to preserve.
buyer: Buyer value to preserve.
notes: Notes value to preserve.
Returns: Returns:
HTMLResponse with 422 status. HTMLResponse with 422 status.
@@ -637,6 +760,10 @@ def _render_sell_error(request, db, locations, products, selected_product_code,
sell_error=error_message, sell_error=error_message,
harvest_action=product_collected, harvest_action=product_collected,
sell_action=product_sold, sell_action=product_sold,
sell_quantity=quantity,
sell_total_price_cents=total_price_cents,
sell_buyer=buyer,
sell_notes=notes,
**display_data, **display_data,
), ),
title="Eggs - AnimalTrack", title="Eggs - AnimalTrack",

View File

@@ -37,6 +37,13 @@ def eggs_page(
cost_per_egg: float | None = None, cost_per_egg: float | None = None,
sales_stats: dict | None = None, sales_stats: dict | None = None,
location_names: dict[str, str] | None = None, location_names: dict[str, str] | None = None,
# Field value preservation on errors
harvest_quantity: str | None = None,
harvest_notes: str | None = None,
sell_quantity: str | None = None,
sell_total_price_cents: str | None = None,
sell_buyer: str | None = None,
sell_notes: str | None = None,
): ):
"""Create the Eggs page with tabbed forms. """Create the Eggs page with tabbed forms.
@@ -56,6 +63,12 @@ def eggs_page(
cost_per_egg: 30-day average cost per egg in EUR. cost_per_egg: 30-day average cost per egg in EUR.
sales_stats: Dict with 'total_qty', 'total_cents', and 'avg_price_per_egg_cents'. sales_stats: Dict with 'total_qty', 'total_cents', and 'avg_price_per_egg_cents'.
location_names: Dict mapping location_id to location name for display. location_names: Dict mapping location_id to location name for display.
harvest_quantity: Preserved quantity value on error.
harvest_notes: Preserved notes value on error.
sell_quantity: Preserved quantity value on error.
sell_total_price_cents: Preserved total price value on error.
sell_buyer: Preserved buyer value on error.
sell_notes: Preserved notes value on error.
Returns: Returns:
Page content with tabbed forms. Page content with tabbed forms.
@@ -85,6 +98,8 @@ def eggs_page(
eggs_per_day=eggs_per_day, eggs_per_day=eggs_per_day,
cost_per_egg=cost_per_egg, cost_per_egg=cost_per_egg,
location_names=location_names, location_names=location_names,
default_quantity=harvest_quantity,
default_notes=harvest_notes,
), ),
cls="uk-active" if harvest_active else None, cls="uk-active" if harvest_active else None,
), ),
@@ -96,6 +111,10 @@ def eggs_page(
action=sell_action, action=sell_action,
recent_events=sell_events, recent_events=sell_events,
sales_stats=sales_stats, sales_stats=sales_stats,
default_quantity=sell_quantity,
default_total_price_cents=sell_total_price_cents,
default_buyer=sell_buyer,
default_notes=sell_notes,
), ),
cls=None if harvest_active else "uk-active", cls=None if harvest_active else "uk-active",
), ),
@@ -113,6 +132,8 @@ def harvest_form(
eggs_per_day: float | None = None, eggs_per_day: float | None = None,
cost_per_egg: float | None = None, cost_per_egg: float | None = None,
location_names: dict[str, str] | None = None, location_names: dict[str, str] | None = None,
default_quantity: str | None = None,
default_notes: str | None = None,
) -> Div: ) -> Div:
"""Create the Harvest form for egg collection. """Create the Harvest form for egg collection.
@@ -125,6 +146,8 @@ def harvest_form(
eggs_per_day: 30-day average eggs per day. eggs_per_day: 30-day average eggs per day.
cost_per_egg: 30-day average cost per egg in EUR. cost_per_egg: 30-day average cost per egg in EUR.
location_names: Dict mapping location_id to location name for display. location_names: Dict mapping location_id to location name for display.
default_quantity: Preserved quantity value on error.
default_notes: Preserved notes value on error.
Returns: Returns:
Div containing form and recent events section. Div containing form and recent events section.
@@ -193,6 +216,7 @@ def harvest_form(
step="1", step="1",
placeholder="Number of eggs", placeholder="Number of eggs",
required=True, required=True,
value=default_quantity or "",
), ),
# Optional notes # Optional notes
LabelTextArea( LabelTextArea(
@@ -200,6 +224,7 @@ def harvest_form(
id="notes", id="notes",
name="notes", name="notes",
placeholder="Optional notes", placeholder="Optional notes",
value=default_notes or "",
), ),
# Event datetime picker (for backdating) # Event datetime picker (for backdating)
event_datetime_field("harvest_datetime"), event_datetime_field("harvest_datetime"),
@@ -231,6 +256,10 @@ def sell_form(
action: Callable[..., Any] | str = "/actions/product-sold", action: Callable[..., Any] | str = "/actions/product-sold",
recent_events: list[tuple[Event, bool]] | None = None, recent_events: list[tuple[Event, bool]] | None = None,
sales_stats: dict | None = None, sales_stats: dict | None = None,
default_quantity: str | None = None,
default_total_price_cents: str | None = None,
default_buyer: str | None = None,
default_notes: str | None = None,
) -> Div: ) -> Div:
"""Create the Sell form for recording sales. """Create the Sell form for recording sales.
@@ -241,6 +270,10 @@ def sell_form(
action: Route function or URL string for form submission. action: Route function or URL string for form submission.
recent_events: Recent (Event, is_deleted) tuples, most recent first. recent_events: Recent (Event, is_deleted) tuples, most recent first.
sales_stats: Dict with 'total_qty' and 'total_cents' for 30-day sales. sales_stats: Dict with 'total_qty' and 'total_cents' for 30-day sales.
default_quantity: Preserved quantity value on error.
default_total_price_cents: Preserved total price value on error.
default_buyer: Preserved buyer value on error.
default_notes: Preserved notes value on error.
Returns: Returns:
Div containing form and recent events section. Div containing form and recent events section.
@@ -315,6 +348,7 @@ def sell_form(
step="1", step="1",
placeholder="Number of items sold", placeholder="Number of items sold",
required=True, required=True,
value=default_quantity or "",
), ),
# Total price in cents # Total price in cents
LabelInput( LabelInput(
@@ -326,6 +360,7 @@ def sell_form(
step="1", step="1",
placeholder="Total price in cents", placeholder="Total price in cents",
required=True, required=True,
value=default_total_price_cents or "",
), ),
# Optional buyer # Optional buyer
LabelInput( LabelInput(
@@ -334,6 +369,7 @@ def sell_form(
name="buyer", name="buyer",
type="text", type="text",
placeholder="Optional buyer name", placeholder="Optional buyer name",
value=default_buyer or "",
), ),
# Optional notes # Optional notes
LabelTextArea( LabelTextArea(
@@ -341,6 +377,7 @@ def sell_form(
id="sell_notes", id="sell_notes",
name="notes", name="notes",
placeholder="Optional notes", placeholder="Optional notes",
value=default_notes or "",
), ),
# Event datetime picker (for backdating) # Event datetime picker (for backdating)
event_datetime_field("sell_datetime"), event_datetime_field("sell_datetime"),