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
if not location_id:
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
@@ -373,12 +380,26 @@ async def product_collected(request: Request, session):
quantity = int(quantity_str)
except ValueError:
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:
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)
@@ -389,7 +410,14 @@ async def product_collected(request: Request, session):
if not resolved_ids:
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
@@ -422,7 +450,16 @@ async def product_collected(request: Request, session):
route="/actions/product-collected",
)
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)
if UserRepository(db).get(actor):
@@ -486,19 +523,48 @@ async def product_sold(request: Request, session):
# Validate 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
try:
quantity = int(quantity_str)
except ValueError:
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:
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
@@ -506,12 +572,30 @@ async def product_sold(request: Request, session):
total_price_cents = int(total_price_str)
except ValueError:
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:
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)
@@ -544,7 +628,18 @@ async def product_sold(request: Request, session):
route="/actions/product-sold",
)
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_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):
"""Render harvest form with error message.
def _render_harvest_error(
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:
request: The HTTP request.
@@ -583,6 +687,8 @@ def _render_harvest_error(request, db, locations, products, selected_location_id
products: List of sellable products.
selected_location_id: Currently selected location.
error_message: Error message to display.
quantity: Quantity value to preserve.
notes: Notes value to preserve.
Returns:
HTMLResponse with 422 status.
@@ -600,6 +706,8 @@ def _render_harvest_error(request, db, locations, products, selected_location_id
harvest_error=error_message,
harvest_action=product_collected,
sell_action=product_sold,
harvest_quantity=quantity,
harvest_notes=notes,
**display_data,
),
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):
"""Render sell form with error message.
def _render_sell_error(
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:
request: The HTTP request.
@@ -620,6 +739,10 @@ def _render_sell_error(request, db, locations, products, selected_product_code,
products: List of sellable products.
selected_product_code: Currently selected product code.
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:
HTMLResponse with 422 status.
@@ -637,6 +760,10 @@ def _render_sell_error(request, db, locations, products, selected_product_code,
sell_error=error_message,
harvest_action=product_collected,
sell_action=product_sold,
sell_quantity=quantity,
sell_total_price_cents=total_price_cents,
sell_buyer=buyer,
sell_notes=notes,
**display_data,
),
title="Eggs - AnimalTrack",

View File

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