feat: add render_page() helper and fix username display

Create render_page() wrapper that auto-extracts auth from request
for page() calls. This eliminates manual username/user_role passing
and ensures consistent auth handling across all routes.

Updated all route files to use render_page():
- actions.py, eggs.py, feed.py, move.py, products.py
- animals.py, events.py, locations.py, registry.py

This fixes "Guest" username display on forms by ensuring auth
is always extracted from request.scope.

🤖 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-01 13:50:28 +00:00
parent b74ca53f20
commit fdbf259182
11 changed files with 243 additions and 151 deletions

View File

@@ -38,7 +38,7 @@ from animaltrack.selection import compute_roster_hash, parse_filter, resolve_fil
from animaltrack.selection.validation import SelectionContext, validate_selection from animaltrack.selection.validation import SelectionContext, validate_selection
from animaltrack.services.animal import AnimalService, ValidationError from animaltrack.services.animal import AnimalService, ValidationError
from animaltrack.web.auth import UserRole, require_role from animaltrack.web.auth import UserRole, require_role
from animaltrack.web.templates import page from animaltrack.web.templates import render_page
from animaltrack.web.templates.actions import ( from animaltrack.web.templates.actions import (
attrs_diff_panel, attrs_diff_panel,
attrs_form, attrs_form,
@@ -110,7 +110,8 @@ def cohort_index(request: Request):
locations = LocationRepository(db).list_active() locations = LocationRepository(db).list_active()
species_list = SpeciesRepository(db).list_active() species_list = SpeciesRepository(db).list_active()
return page( return render_page(
request,
cohort_form(locations, species_list), cohort_form(locations, species_list),
title="Create Cohort - AnimalTrack", title="Create Cohort - AnimalTrack",
active_nav=None, active_nav=None,
@@ -141,20 +142,32 @@ async def animal_cohort(request: Request):
try: try:
count = int(count_str) count = int(count_str)
if count < 1: if count < 1:
return _render_cohort_error(locations, species_list, "Count must be at least 1", form) return _render_cohort_error(
request, locations, species_list, "Count must be at least 1", form
)
except ValueError: except ValueError:
return _render_cohort_error(locations, species_list, "Count must be a valid number", form) return _render_cohort_error(
request, locations, species_list, "Count must be a valid number", form
)
# Validate required fields - check for empty or placeholder values # Validate required fields - check for empty or placeholder values
# Note: disabled placeholder options may send their text instead of empty value # Note: disabled placeholder options may send their text instead of empty value
if not species or species not in ("duck", "goose"): if not species or species not in ("duck", "goose"):
return _render_cohort_error(locations, species_list, "Please select a species", form) return _render_cohort_error(
request, locations, species_list, "Please select a species", form
)
if not location_id or len(location_id) != 26: if not location_id or len(location_id) != 26:
return _render_cohort_error(locations, species_list, "Please select a location", form) return _render_cohort_error(
request, locations, species_list, "Please select a location", form
)
if not life_stage or life_stage not in ("hatchling", "juvenile", "subadult", "adult"): if not life_stage or life_stage not in ("hatchling", "juvenile", "subadult", "adult"):
return _render_cohort_error(locations, species_list, "Please select a life stage", form) return _render_cohort_error(
request, locations, species_list, "Please select a life stage", form
)
if not origin or origin not in ("hatched", "purchased", "rescued", "unknown"): if not origin or origin not in ("hatched", "purchased", "rescued", "unknown"):
return _render_cohort_error(locations, species_list, "Please select an origin", form) return _render_cohort_error(
request, locations, species_list, "Please select an origin", form
)
# Create payload # Create payload
try: try:
@@ -168,7 +181,7 @@ async def animal_cohort(request: Request):
notes=notes, notes=notes,
) )
except Exception as e: except Exception as e:
return _render_cohort_error(locations, species_list, str(e), form) return _render_cohort_error(request, locations, species_list, str(e), form)
# Get actor from auth # Get actor from auth
auth = request.scope.get("auth") auth = request.scope.get("auth")
@@ -183,12 +196,13 @@ async def animal_cohort(request: Request):
payload, ts_utc, actor, nonce=nonce, route="/actions/animal-cohort" payload, ts_utc, actor, nonce=nonce, route="/actions/animal-cohort"
) )
except ValidationError as e: except ValidationError as e:
return _render_cohort_error(locations, species_list, str(e), form) return _render_cohort_error(request, locations, species_list, str(e), form)
# Success: re-render fresh form # Success: re-render fresh form
response = HTMLResponse( response = HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
cohort_form(locations, species_list), cohort_form(locations, species_list),
title="Create Cohort - AnimalTrack", title="Create Cohort - AnimalTrack",
active_nav=None, active_nav=None,
@@ -211,6 +225,7 @@ async def animal_cohort(request: Request):
def _render_cohort_error( def _render_cohort_error(
request: Request,
locations: list, locations: list,
species_list: list, species_list: list,
error_message: str, error_message: str,
@@ -219,7 +234,8 @@ def _render_cohort_error(
"""Render cohort form with error message.""" """Render cohort form with error message."""
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
cohort_form( cohort_form(
locations, locations,
species_list, species_list,
@@ -255,7 +271,8 @@ def hatch_index(request: Request):
locations = LocationRepository(db).list_active() locations = LocationRepository(db).list_active()
species_list = SpeciesRepository(db).list_active() species_list = SpeciesRepository(db).list_active()
return page( return render_page(
request,
hatch_form(locations, species_list), hatch_form(locations, species_list),
title="Record Hatch - AnimalTrack", title="Record Hatch - AnimalTrack",
active_nav=None, active_nav=None,
@@ -285,18 +302,22 @@ async def hatch_recorded(request: Request):
hatched_live = int(hatched_live_str) hatched_live = int(hatched_live_str)
if hatched_live < 1: if hatched_live < 1:
return _render_hatch_error( return _render_hatch_error(
locations, species_list, "Hatched count must be at least 1", form request, locations, species_list, "Hatched count must be at least 1", form
) )
except ValueError: except ValueError:
return _render_hatch_error( return _render_hatch_error(
locations, species_list, "Hatched count must be a valid number", form request, locations, species_list, "Hatched count must be a valid number", form
) )
# Validate required fields # Validate required fields
if not species: if not species:
return _render_hatch_error(locations, species_list, "Please select a species", form) return _render_hatch_error(
request, locations, species_list, "Please select a species", form
)
if not location_id: if not location_id:
return _render_hatch_error(locations, species_list, "Please select a hatch location", form) return _render_hatch_error(
request, locations, species_list, "Please select a hatch location", form
)
# Create payload # Create payload
try: try:
@@ -308,7 +329,7 @@ async def hatch_recorded(request: Request):
notes=notes, notes=notes,
) )
except Exception as e: except Exception as e:
return _render_hatch_error(locations, species_list, str(e), form) return _render_hatch_error(request, locations, species_list, str(e), form)
# Get actor from auth # Get actor from auth
auth = request.scope.get("auth") auth = request.scope.get("auth")
@@ -323,12 +344,13 @@ async def hatch_recorded(request: Request):
payload, ts_utc, actor, nonce=nonce, route="/actions/hatch-recorded" payload, ts_utc, actor, nonce=nonce, route="/actions/hatch-recorded"
) )
except ValidationError as e: except ValidationError as e:
return _render_hatch_error(locations, species_list, str(e), form) return _render_hatch_error(request, locations, species_list, str(e), form)
# Success: re-render fresh form # Success: re-render fresh form
response = HTMLResponse( response = HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
hatch_form(locations, species_list), hatch_form(locations, species_list),
title="Record Hatch - AnimalTrack", title="Record Hatch - AnimalTrack",
active_nav=None, active_nav=None,
@@ -351,6 +373,7 @@ async def hatch_recorded(request: Request):
def _render_hatch_error( def _render_hatch_error(
request: Request,
locations: list, locations: list,
species_list: list, species_list: list,
error_message: str, error_message: str,
@@ -359,7 +382,8 @@ def _render_hatch_error(
"""Render hatch form with error message.""" """Render hatch form with error message."""
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
hatch_form( hatch_form(
locations, locations,
species_list, species_list,
@@ -402,7 +426,8 @@ def promote_index(request: Request, animal_id: str):
if animal.identified: if animal.identified:
return HTMLResponse(content="Animal is already identified", status_code=400) return HTMLResponse(content="Animal is already identified", status_code=400)
return page( return render_page(
request,
promote_form(animal), promote_form(animal),
title="Promote Animal - AnimalTrack", title="Promote Animal - AnimalTrack",
active_nav=None, active_nav=None,
@@ -436,10 +461,10 @@ async def animal_promote(request: Request):
return HTMLResponse(content="Animal not found", status_code=404) return HTMLResponse(content="Animal not found", status_code=404)
if animal.status != "alive": if animal.status != "alive":
return _render_promote_error(animal, "Only alive animals can be promoted", form) return _render_promote_error(request, animal, "Only alive animals can be promoted", form)
if animal.identified: if animal.identified:
return _render_promote_error(animal, "Animal is already identified", form) return _render_promote_error(request, animal, "Animal is already identified", form)
# Create payload # Create payload
try: try:
@@ -452,7 +477,7 @@ async def animal_promote(request: Request):
notes=notes, notes=notes,
) )
except Exception as e: except Exception as e:
return _render_promote_error(animal, str(e), form) return _render_promote_error(request, animal, str(e), form)
# Get actor from auth # Get actor from auth
auth = request.scope.get("auth") auth = request.scope.get("auth")
@@ -465,7 +490,7 @@ async def animal_promote(request: Request):
try: try:
service.promote_animal(payload, ts_utc, actor, nonce=nonce, route="/actions/animal-promote") service.promote_animal(payload, ts_utc, actor, nonce=nonce, route="/actions/animal-promote")
except ValidationError as e: except ValidationError as e:
return _render_promote_error(animal, str(e), form) return _render_promote_error(request, animal, str(e), form)
# Success: redirect to animal detail page # Success: redirect to animal detail page
from starlette.responses import RedirectResponse from starlette.responses import RedirectResponse
@@ -479,6 +504,7 @@ async def animal_promote(request: Request):
def _render_promote_error( def _render_promote_error(
request: Request,
animal: Any, animal: Any,
error_message: str, error_message: str,
form_data: Any = None, form_data: Any = None,
@@ -486,7 +512,8 @@ def _render_promote_error(
"""Render promote form with error message.""" """Render promote form with error message."""
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
promote_form( promote_form(
animal, animal,
error=error_message, error=error_message,
@@ -529,7 +556,8 @@ def tag_add_index(request: Request):
if resolved_ids: if resolved_ids:
roster_hash = compute_roster_hash(resolved_ids, None) roster_hash = compute_roster_hash(resolved_ids, None)
return page( return render_page(
request,
tag_add_form( tag_add_form(
filter_str=filter_str, filter_str=filter_str,
resolved_ids=resolved_ids, resolved_ids=resolved_ids,
@@ -563,11 +591,11 @@ async def animal_tag_add(request: Request):
# Validation: tag required # Validation: tag required
if not tag: if not tag:
return _render_tag_add_error_form(db, filter_str, "Please enter a tag") return _render_tag_add_error_form(request, db, filter_str, "Please enter a tag")
# Validation: must have animals # Validation: must have animals
if not resolved_ids: if not resolved_ids:
return _render_tag_add_error_form(db, filter_str, "No animals selected") return _render_tag_add_error_form(request, db, filter_str, "No animals selected")
# Build selection context for validation # Build selection context for validation
context = SelectionContext( context = SelectionContext(
@@ -586,7 +614,8 @@ async def animal_tag_add(request: Request):
# Mismatch detected - return 409 with diff panel # Mismatch detected - return 409 with diff panel
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
tag_add_diff_panel( tag_add_diff_panel(
diff=result.diff, diff=result.diff,
filter_str=filter_str, filter_str=filter_str,
@@ -613,7 +642,7 @@ async def animal_tag_add(request: Request):
# Check we still have animals # Check we still have animals
if not ids_to_tag: if not ids_to_tag:
return _render_tag_add_error_form(db, filter_str, "No animals remaining to tag") return _render_tag_add_error_form(request, db, filter_str, "No animals remaining to tag")
# Create payload # Create payload
try: try:
@@ -622,7 +651,7 @@ async def animal_tag_add(request: Request):
tag=tag, tag=tag,
) )
except Exception as e: except Exception as e:
return _render_tag_add_error_form(db, filter_str, str(e)) return _render_tag_add_error_form(request, db, filter_str, str(e))
# Get actor from auth # Get actor from auth
auth = request.scope.get("auth") auth = request.scope.get("auth")
@@ -636,12 +665,13 @@ async def animal_tag_add(request: Request):
payload, ts_utc, actor, nonce=nonce, route="/actions/animal-tag-add" payload, ts_utc, actor, nonce=nonce, route="/actions/animal-tag-add"
) )
except ValidationError as e: except ValidationError as e:
return _render_tag_add_error_form(db, filter_str, str(e)) return _render_tag_add_error_form(request, db, filter_str, str(e))
# Success: re-render fresh form # Success: re-render fresh form
response = HTMLResponse( response = HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
tag_add_form(), tag_add_form(),
title="Add Tag - AnimalTrack", title="Add Tag - AnimalTrack",
active_nav=None, active_nav=None,
@@ -663,7 +693,7 @@ async def animal_tag_add(request: Request):
return response return response
def _render_tag_add_error_form(db, filter_str, error_message): def _render_tag_add_error_form(request, db, filter_str, error_message):
"""Render tag add form with error message.""" """Render tag add form with error message."""
# Re-resolve to show current selection info # Re-resolve to show current selection info
ts_utc = int(time.time() * 1000) ts_utc = int(time.time() * 1000)
@@ -680,7 +710,8 @@ def _render_tag_add_error_form(db, filter_str, error_message):
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
tag_add_form( tag_add_form(
filter_str=filter_str, filter_str=filter_str,
resolved_ids=resolved_ids, resolved_ids=resolved_ids,
@@ -753,7 +784,8 @@ def tag_end_index(request: Request):
roster_hash = compute_roster_hash(resolved_ids, None) roster_hash = compute_roster_hash(resolved_ids, None)
active_tags = _get_active_tags_for_animals(db, resolved_ids) active_tags = _get_active_tags_for_animals(db, resolved_ids)
return page( return render_page(
request,
tag_end_form( tag_end_form(
filter_str=filter_str, filter_str=filter_str,
resolved_ids=resolved_ids, resolved_ids=resolved_ids,
@@ -788,11 +820,11 @@ async def animal_tag_end(request: Request):
# Validation: tag required # Validation: tag required
if not tag: if not tag:
return _render_tag_end_error_form(db, filter_str, "Please select a tag to end") return _render_tag_end_error_form(request, db, filter_str, "Please select a tag to end")
# Validation: must have animals # Validation: must have animals
if not resolved_ids: if not resolved_ids:
return _render_tag_end_error_form(db, filter_str, "No animals selected") return _render_tag_end_error_form(request, db, filter_str, "No animals selected")
# Build selection context for validation # Build selection context for validation
context = SelectionContext( context = SelectionContext(
@@ -811,7 +843,8 @@ async def animal_tag_end(request: Request):
# Mismatch detected - return 409 with diff panel # Mismatch detected - return 409 with diff panel
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
tag_end_diff_panel( tag_end_diff_panel(
diff=result.diff, diff=result.diff,
filter_str=filter_str, filter_str=filter_str,
@@ -838,7 +871,7 @@ async def animal_tag_end(request: Request):
# Check we still have animals # Check we still have animals
if not ids_to_untag: if not ids_to_untag:
return _render_tag_end_error_form(db, filter_str, "No animals remaining") return _render_tag_end_error_form(request, db, filter_str, "No animals remaining")
# Create payload # Create payload
try: try:
@@ -847,7 +880,7 @@ async def animal_tag_end(request: Request):
tag=tag, tag=tag,
) )
except Exception as e: except Exception as e:
return _render_tag_end_error_form(db, filter_str, str(e)) return _render_tag_end_error_form(request, db, filter_str, str(e))
# Get actor from auth # Get actor from auth
auth = request.scope.get("auth") auth = request.scope.get("auth")
@@ -861,12 +894,13 @@ async def animal_tag_end(request: Request):
payload, ts_utc, actor, nonce=nonce, route="/actions/animal-tag-end" payload, ts_utc, actor, nonce=nonce, route="/actions/animal-tag-end"
) )
except ValidationError as e: except ValidationError as e:
return _render_tag_end_error_form(db, filter_str, str(e)) return _render_tag_end_error_form(request, db, filter_str, str(e))
# Success: re-render fresh form # Success: re-render fresh form
response = HTMLResponse( response = HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
tag_end_form(), tag_end_form(),
title="End Tag - AnimalTrack", title="End Tag - AnimalTrack",
active_nav=None, active_nav=None,
@@ -888,7 +922,7 @@ async def animal_tag_end(request: Request):
return response return response
def _render_tag_end_error_form(db, filter_str, error_message): def _render_tag_end_error_form(request, db, filter_str, error_message):
"""Render tag end form with error message.""" """Render tag end form with error message."""
# Re-resolve to show current selection info # Re-resolve to show current selection info
ts_utc = int(time.time() * 1000) ts_utc = int(time.time() * 1000)
@@ -907,7 +941,8 @@ def _render_tag_end_error_form(db, filter_str, error_message):
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
tag_end_form( tag_end_form(
filter_str=filter_str, filter_str=filter_str,
resolved_ids=resolved_ids, resolved_ids=resolved_ids,
@@ -951,7 +986,8 @@ def attrs_index(request: Request):
if resolved_ids: if resolved_ids:
roster_hash = compute_roster_hash(resolved_ids, None) roster_hash = compute_roster_hash(resolved_ids, None)
return page( return render_page(
request,
attrs_form( attrs_form(
filter_str=filter_str, filter_str=filter_str,
resolved_ids=resolved_ids, resolved_ids=resolved_ids,
@@ -988,12 +1024,12 @@ async def animal_attrs(request: Request):
# Validation: at least one attribute required # Validation: at least one attribute required
if not sex and not life_stage and not repro_status: if not sex and not life_stage and not repro_status:
return _render_attrs_error_form( return _render_attrs_error_form(
db, filter_str, "Please select at least one attribute to update" request, db, filter_str, "Please select at least one attribute to update"
) )
# Validation: must have animals # Validation: must have animals
if not resolved_ids: if not resolved_ids:
return _render_attrs_error_form(db, filter_str, "No animals selected") return _render_attrs_error_form(request, db, filter_str, "No animals selected")
# Build selection context for validation # Build selection context for validation
context = SelectionContext( context = SelectionContext(
@@ -1012,7 +1048,8 @@ async def animal_attrs(request: Request):
# Mismatch detected - return 409 with diff panel # Mismatch detected - return 409 with diff panel
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
attrs_diff_panel( attrs_diff_panel(
diff=result.diff, diff=result.diff,
filter_str=filter_str, filter_str=filter_str,
@@ -1041,7 +1078,7 @@ async def animal_attrs(request: Request):
# Check we still have animals # Check we still have animals
if not ids_to_update: if not ids_to_update:
return _render_attrs_error_form(db, filter_str, "No animals remaining") return _render_attrs_error_form(request, db, filter_str, "No animals remaining")
# Create payload # Create payload
try: try:
@@ -1055,7 +1092,7 @@ async def animal_attrs(request: Request):
set=attr_set, set=attr_set,
) )
except Exception as e: except Exception as e:
return _render_attrs_error_form(db, filter_str, str(e)) return _render_attrs_error_form(request, db, filter_str, str(e))
# Get actor from auth # Get actor from auth
auth = request.scope.get("auth") auth = request.scope.get("auth")
@@ -1069,12 +1106,13 @@ async def animal_attrs(request: Request):
payload, ts_utc, actor, nonce=nonce, route="/actions/animal-attrs" payload, ts_utc, actor, nonce=nonce, route="/actions/animal-attrs"
) )
except ValidationError as e: except ValidationError as e:
return _render_attrs_error_form(db, filter_str, str(e)) return _render_attrs_error_form(request, db, filter_str, str(e))
# Success: re-render fresh form # Success: re-render fresh form
response = HTMLResponse( response = HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
attrs_form(), attrs_form(),
title="Update Attributes - AnimalTrack", title="Update Attributes - AnimalTrack",
active_nav=None, active_nav=None,
@@ -1096,7 +1134,7 @@ async def animal_attrs(request: Request):
return response return response
def _render_attrs_error_form(db, filter_str, error_message): def _render_attrs_error_form(request, db, filter_str, error_message):
"""Render attributes form with error message.""" """Render attributes form with error message."""
# Re-resolve to show current selection info # Re-resolve to show current selection info
ts_utc = int(time.time() * 1000) ts_utc = int(time.time() * 1000)
@@ -1113,7 +1151,8 @@ def _render_attrs_error_form(db, filter_str, error_message):
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
attrs_form( attrs_form(
filter_str=filter_str, filter_str=filter_str,
resolved_ids=resolved_ids, resolved_ids=resolved_ids,
@@ -1160,7 +1199,8 @@ def outcome_index(request: Request):
product_repo = ProductRepository(db) product_repo = ProductRepository(db)
products = [(p.code, p.name) for p in product_repo.list_all() if p.active] products = [(p.code, p.name) for p in product_repo.list_all() if p.active]
return page( return render_page(
request,
outcome_form( outcome_form(
filter_str=filter_str, filter_str=filter_str,
resolved_ids=resolved_ids, resolved_ids=resolved_ids,
@@ -1218,17 +1258,19 @@ async def animal_outcome(request: Request):
# Validation: outcome required # Validation: outcome required
if not outcome_str: if not outcome_str:
return _render_outcome_error_form(db, filter_str, "Please select an outcome") return _render_outcome_error_form(request, db, filter_str, "Please select an outcome")
# Validate outcome is valid enum value # Validate outcome is valid enum value
try: try:
outcome_enum = Outcome(outcome_str) outcome_enum = Outcome(outcome_str)
except ValueError: except ValueError:
return _render_outcome_error_form(db, filter_str, f"Invalid outcome: {outcome_str}") return _render_outcome_error_form(
request, db, filter_str, f"Invalid outcome: {outcome_str}"
)
# Validation: must have animals # Validation: must have animals
if not resolved_ids: if not resolved_ids:
return _render_outcome_error_form(db, filter_str, "No animals selected") return _render_outcome_error_form(request, db, filter_str, "No animals selected")
# Build selection context for validation # Build selection context for validation
context = SelectionContext( context = SelectionContext(
@@ -1247,7 +1289,8 @@ async def animal_outcome(request: Request):
# Mismatch detected - return 409 with diff panel # Mismatch detected - return 409 with diff panel
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
outcome_diff_panel( outcome_diff_panel(
diff=result.diff, diff=result.diff,
filter_str=filter_str, filter_str=filter_str,
@@ -1279,7 +1322,7 @@ async def animal_outcome(request: Request):
# Check we still have animals # Check we still have animals
if not ids_to_update: if not ids_to_update:
return _render_outcome_error_form(db, filter_str, "No animals remaining") return _render_outcome_error_form(request, db, filter_str, "No animals remaining")
# Build yield items if provided # Build yield items if provided
yield_items: list[YieldItem] | None = None yield_items: list[YieldItem] | None = None
@@ -1307,7 +1350,7 @@ async def animal_outcome(request: Request):
notes=notes, notes=notes,
) )
except Exception as e: except Exception as e:
return _render_outcome_error_form(db, filter_str, str(e)) return _render_outcome_error_form(request, db, filter_str, str(e))
# Get actor from auth # Get actor from auth
auth = request.scope.get("auth") auth = request.scope.get("auth")
@@ -1321,7 +1364,7 @@ async def animal_outcome(request: Request):
payload, ts_utc, actor, nonce=nonce, route="/actions/animal-outcome" payload, ts_utc, actor, nonce=nonce, route="/actions/animal-outcome"
) )
except ValidationError as e: except ValidationError as e:
return _render_outcome_error_form(db, filter_str, str(e)) return _render_outcome_error_form(request, db, filter_str, str(e))
# Success: re-render fresh form # Success: re-render fresh form
product_repo = ProductRepository(db) product_repo = ProductRepository(db)
@@ -1329,7 +1372,8 @@ async def animal_outcome(request: Request):
response = HTMLResponse( response = HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
outcome_form( outcome_form(
filter_str="", filter_str="",
resolved_ids=[], resolved_ids=[],
@@ -1358,7 +1402,7 @@ async def animal_outcome(request: Request):
return response return response
def _render_outcome_error_form(db, filter_str, error_message): def _render_outcome_error_form(request, db, filter_str, error_message):
"""Render outcome form with error message.""" """Render outcome form with error message."""
# Re-resolve to show current selection info # Re-resolve to show current selection info
ts_utc = int(time.time() * 1000) ts_utc = int(time.time() * 1000)
@@ -1379,7 +1423,8 @@ def _render_outcome_error_form(db, filter_str, error_message):
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
outcome_form( outcome_form(
filter_str=filter_str, filter_str=filter_str,
resolved_ids=resolved_ids, resolved_ids=resolved_ids,
@@ -1424,7 +1469,8 @@ async def status_correct_index(req: Request):
if resolved_ids: if resolved_ids:
roster_hash = compute_roster_hash(resolved_ids, None) roster_hash = compute_roster_hash(resolved_ids, None)
return page( return render_page(
req,
status_correct_form( status_correct_form(
filter_str=filter_str, filter_str=filter_str,
resolved_ids=resolved_ids, resolved_ids=resolved_ids,
@@ -1461,23 +1507,23 @@ async def animal_status_correct(req: Request):
# Validation: new_status required # Validation: new_status required
if not new_status_str: if not new_status_str:
return _render_status_correct_error_form(db, filter_str, "Please select a new status") return _render_status_correct_error_form(req, db, filter_str, "Please select a new status")
# Validate status is valid enum value # Validate status is valid enum value
try: try:
new_status_enum = AnimalStatus(new_status_str) new_status_enum = AnimalStatus(new_status_str)
except ValueError: except ValueError:
return _render_status_correct_error_form( return _render_status_correct_error_form(
db, filter_str, f"Invalid status: {new_status_str}" req, db, filter_str, f"Invalid status: {new_status_str}"
) )
# Validation: reason required for admin actions # Validation: reason required for admin actions
if not reason: if not reason:
return _render_status_correct_error_form(db, filter_str, "Reason is required") return _render_status_correct_error_form(req, db, filter_str, "Reason is required")
# Validation: must have animals # Validation: must have animals
if not resolved_ids: if not resolved_ids:
return _render_status_correct_error_form(db, filter_str, "No animals selected") return _render_status_correct_error_form(req, db, filter_str, "No animals selected")
# Build selection context for validation # Build selection context for validation
context = SelectionContext( context = SelectionContext(
@@ -1496,7 +1542,8 @@ async def animal_status_correct(req: Request):
# Mismatch detected - return 409 with diff panel # Mismatch detected - return 409 with diff panel
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
req,
status_correct_diff_panel( status_correct_diff_panel(
diff=result.diff, diff=result.diff,
filter_str=filter_str, filter_str=filter_str,
@@ -1524,7 +1571,7 @@ async def animal_status_correct(req: Request):
# Check we still have animals # Check we still have animals
if not ids_to_update: if not ids_to_update:
return _render_status_correct_error_form(db, filter_str, "No animals remaining") return _render_status_correct_error_form(req, db, filter_str, "No animals remaining")
# Create payload # Create payload
try: try:
@@ -1535,7 +1582,7 @@ async def animal_status_correct(req: Request):
notes=notes, notes=notes,
) )
except Exception as e: except Exception as e:
return _render_status_correct_error_form(db, filter_str, str(e)) return _render_status_correct_error_form(req, db, filter_str, str(e))
# Get actor from auth # Get actor from auth
auth = req.scope.get("auth") auth = req.scope.get("auth")
@@ -1549,12 +1596,13 @@ async def animal_status_correct(req: Request):
payload, ts_utc, actor, nonce=nonce, route="/actions/animal-status-correct" payload, ts_utc, actor, nonce=nonce, route="/actions/animal-status-correct"
) )
except ValidationError as e: except ValidationError as e:
return _render_status_correct_error_form(db, filter_str, str(e)) return _render_status_correct_error_form(req, db, filter_str, str(e))
# Success: re-render fresh form # Success: re-render fresh form
response = HTMLResponse( response = HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
req,
status_correct_form( status_correct_form(
filter_str="", filter_str="",
resolved_ids=[], resolved_ids=[],
@@ -1582,7 +1630,7 @@ async def animal_status_correct(req: Request):
return response return response
def _render_status_correct_error_form(db, filter_str, error_message): def _render_status_correct_error_form(request, db, filter_str, error_message):
"""Render status correct form with error message.""" """Render status correct form with error message."""
# Re-resolve to show current selection info # Re-resolve to show current selection info
ts_utc = int(time.time() * 1000) ts_utc = int(time.time() * 1000)
@@ -1599,7 +1647,8 @@ def _render_status_correct_error_form(db, filter_str, error_message):
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
status_correct_form( status_correct_form(
filter_str=filter_str, filter_str=filter_str,
resolved_ids=resolved_ids, resolved_ids=resolved_ids,

View File

@@ -7,7 +7,7 @@ from starlette.responses import HTMLResponse
from animaltrack.repositories.animal_timeline import AnimalTimelineRepository from animaltrack.repositories.animal_timeline import AnimalTimelineRepository
from animaltrack.web.templates.animal_detail import animal_detail_page from animaltrack.web.templates.animal_detail import animal_detail_page
from animaltrack.web.templates.base import page from animaltrack.web.templates.base import render_page
# APIRouter for multi-file route organization # APIRouter for multi-file route organization
ar = APIRouter() ar = APIRouter()
@@ -40,7 +40,8 @@ def animal_detail(request: Request, animal_id: str):
title = f"{display_name} - AnimalTrack" title = f"{display_name} - AnimalTrack"
# Full page render # Full page render
return page( return render_page(
request,
animal_detail_page(animal, timeline, merge_info), animal_detail_page(animal, timeline, merge_info),
title=title, title=title,
active_nav=None, active_nav=None,

View File

@@ -24,7 +24,7 @@ from animaltrack.repositories.products import ProductRepository
from animaltrack.repositories.user_defaults import UserDefaultsRepository from animaltrack.repositories.user_defaults import UserDefaultsRepository
from animaltrack.repositories.users import UserRepository from animaltrack.repositories.users import UserRepository
from animaltrack.services.products import ProductService, ValidationError from animaltrack.services.products import ProductService, ValidationError
from animaltrack.web.templates import page from animaltrack.web.templates import render_page
from animaltrack.web.templates.eggs import eggs_page from animaltrack.web.templates.eggs import eggs_page
# APIRouter for multi-file route organization # APIRouter for multi-file route organization
@@ -78,10 +78,9 @@ def egg_index(request: Request):
locations = LocationRepository(db).list_active() locations = LocationRepository(db).list_active()
products = _get_sellable_products(db) products = _get_sellable_products(db)
# Get auth info for user role # Get auth info for user defaults
auth = request.scope.get("auth") auth = request.scope.get("auth")
username = auth.username if auth else None username = auth.username if auth else None
user_role = auth.role if auth else None
# Check for active tab from query params # Check for active tab from query params
active_tab = request.query_params.get("tab", "harvest") active_tab = request.query_params.get("tab", "harvest")
@@ -97,7 +96,8 @@ def egg_index(request: Request):
if defaults: if defaults:
selected_location_id = defaults.location_id selected_location_id = defaults.location_id
return page( return render_page(
request,
eggs_page( eggs_page(
locations, locations,
products, products,
@@ -108,8 +108,6 @@ def egg_index(request: Request):
), ),
title="Eggs - AnimalTrack", title="Eggs - AnimalTrack",
active_nav="eggs", active_nav="eggs",
user_role=user_role,
username=username,
) )
@@ -122,7 +120,6 @@ async def product_collected(request: Request):
# Get auth info # Get auth info
auth = request.scope.get("auth") auth = request.scope.get("auth")
actor = auth.username if auth else "unknown" actor = auth.username if auth else "unknown"
user_role = auth.role if auth else None
# Extract form data # Extract form data
location_id = form.get("location_id", "") location_id = form.get("location_id", "")
@@ -208,7 +205,8 @@ async def product_collected(request: Request):
# Success: re-render form with location sticking, qty cleared # Success: re-render form with location sticking, qty cleared
response = HTMLResponse( response = HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
eggs_page( eggs_page(
locations, locations,
products, products,
@@ -219,8 +217,6 @@ async def product_collected(request: Request):
), ),
title="Eggs - AnimalTrack", title="Eggs - AnimalTrack",
active_nav="eggs", active_nav="eggs",
user_role=user_role,
username=actor,
) )
), ),
) )
@@ -242,7 +238,6 @@ async def product_sold(request: Request):
# Get auth info # Get auth info
auth = request.scope.get("auth") auth = request.scope.get("auth")
actor = auth.username if auth else "unknown" actor = auth.username if auth else "unknown"
user_role = auth.role if auth else None
# Extract form data # Extract form data
product_code = form.get("product_code", "") product_code = form.get("product_code", "")
@@ -321,7 +316,8 @@ async def product_sold(request: Request):
# Success: re-render form with product sticking # Success: re-render form with product sticking
response = HTMLResponse( response = HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
eggs_page( eggs_page(
locations, locations,
products, products,
@@ -332,8 +328,6 @@ async def product_sold(request: Request):
), ),
title="Eggs - AnimalTrack", title="Eggs - AnimalTrack",
active_nav="eggs", active_nav="eggs",
user_role=user_role,
username=actor,
) )
), ),
) )
@@ -359,13 +353,10 @@ def _render_harvest_error(request, locations, products, selected_location_id, er
Returns: Returns:
HTMLResponse with 422 status. HTMLResponse with 422 status.
""" """
auth = request.scope.get("auth")
user_role = auth.role if auth else None
username = auth.username if auth else None
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
eggs_page( eggs_page(
locations, locations,
products, products,
@@ -377,8 +368,6 @@ def _render_harvest_error(request, locations, products, selected_location_id, er
), ),
title="Eggs - AnimalTrack", title="Eggs - AnimalTrack",
active_nav="eggs", active_nav="eggs",
user_role=user_role,
username=username,
) )
), ),
status_code=422, status_code=422,
@@ -398,13 +387,10 @@ def _render_sell_error(request, locations, products, selected_product_code, erro
Returns: Returns:
HTMLResponse with 422 status. HTMLResponse with 422 status.
""" """
auth = request.scope.get("auth")
user_role = auth.role if auth else None
username = auth.username if auth else None
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
eggs_page( eggs_page(
locations, locations,
products, products,
@@ -416,8 +402,6 @@ def _render_sell_error(request, locations, products, selected_product_code, erro
), ),
title="Eggs - AnimalTrack", title="Eggs - AnimalTrack",
active_nav="eggs", active_nav="eggs",
user_role=user_role,
username=username,
) )
), ),
status_code=422, status_code=422,

View File

@@ -12,7 +12,7 @@ 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.repositories.user_defaults import UserDefaultsRepository
from animaltrack.web.templates import page from animaltrack.web.templates import render_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
# APIRouter for multi-file route organization # APIRouter for multi-file route organization
@@ -61,10 +61,9 @@ 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 # Get username for user defaults lookup
auth = request.scope.get("auth") auth = request.scope.get("auth")
username = auth.username if auth else None 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")
@@ -100,10 +99,9 @@ def event_log_index(request: Request):
return HTMLResponse(content=to_xml(event_log_list(events))) return HTMLResponse(content=to_xml(event_log_list(events)))
# Full page render # Full page render
return page( return render_page(
request,
event_log_panel(events, locations, location_id), event_log_panel(events, locations, location_id),
title="Event Log - AnimalTrack", title="Event Log - AnimalTrack",
active_nav="event_log", active_nav="event_log",
user_role=user_role,
username=username,
) )

View File

@@ -21,7 +21,7 @@ from animaltrack.repositories.locations import LocationRepository
from animaltrack.repositories.user_defaults import UserDefaultsRepository from animaltrack.repositories.user_defaults import UserDefaultsRepository
from animaltrack.repositories.users import UserRepository from animaltrack.repositories.users import UserRepository
from animaltrack.services.feed import FeedService, ValidationError from animaltrack.services.feed import FeedService, ValidationError
from animaltrack.web.templates import page from animaltrack.web.templates import render_page
from animaltrack.web.templates.feed import feed_page from animaltrack.web.templates.feed import feed_page
@@ -91,7 +91,8 @@ def feed_index(request: Request):
selected_feed_type_code = defaults.feed_type_code selected_feed_type_code = defaults.feed_type_code
default_amount_kg = defaults.amount_kg default_amount_kg = defaults.amount_kg
return page( return render_page(
request,
feed_page( feed_page(
locations, locations,
feed_types, feed_types,
@@ -134,6 +135,7 @@ async def feed_given(request: Request):
# Validate location_id # Validate location_id
if not location_id: if not location_id:
return _render_give_error( return _render_give_error(
request,
locations, locations,
feed_types, feed_types,
"Please select a location", "Please select a location",
@@ -144,6 +146,7 @@ async def feed_given(request: Request):
# Validate feed_type_code # Validate feed_type_code
if not feed_type_code: if not feed_type_code:
return _render_give_error( return _render_give_error(
request,
locations, locations,
feed_types, feed_types,
"Please select a feed type", "Please select a feed type",
@@ -156,6 +159,7 @@ async def feed_given(request: Request):
amount_kg = int(amount_kg_str) amount_kg = int(amount_kg_str)
except ValueError: except ValueError:
return _render_give_error( return _render_give_error(
request,
locations, locations,
feed_types, feed_types,
"Amount must be a number", "Amount must be a number",
@@ -165,6 +169,7 @@ async def feed_given(request: Request):
if amount_kg < 1: if amount_kg < 1:
return _render_give_error( return _render_give_error(
request,
locations, locations,
feed_types, feed_types,
"Amount must be at least 1 kg", "Amount must be at least 1 kg",
@@ -206,6 +211,7 @@ async def feed_given(request: Request):
) )
except ValidationError as e: except ValidationError as e:
return _render_give_error( return _render_give_error(
request,
locations, locations,
feed_types, feed_types,
str(e), str(e),
@@ -235,7 +241,8 @@ async def feed_given(request: Request):
# Success: re-render form with location/type sticking, amount reset # Success: re-render form with location/type sticking, amount reset
response = HTMLResponse( response = HTMLResponse(
content=str( content=str(
page( render_page(
request,
feed_page( feed_page(
locations, locations,
feed_types, feed_types,
@@ -288,6 +295,7 @@ async def feed_purchased(request: Request):
# Validate feed_type_code # Validate feed_type_code
if not feed_type_code: if not feed_type_code:
return _render_purchase_error( return _render_purchase_error(
request,
locations, locations,
feed_types, feed_types,
"Please select a feed type", "Please select a feed type",
@@ -298,6 +306,7 @@ async def feed_purchased(request: Request):
bag_size_kg = int(bag_size_kg_str) bag_size_kg = int(bag_size_kg_str)
except ValueError: except ValueError:
return _render_purchase_error( return _render_purchase_error(
request,
locations, locations,
feed_types, feed_types,
"Bag size must be a number", "Bag size must be a number",
@@ -305,6 +314,7 @@ async def feed_purchased(request: Request):
if bag_size_kg < 1: if bag_size_kg < 1:
return _render_purchase_error( return _render_purchase_error(
request,
locations, locations,
feed_types, feed_types,
"Bag size must be at least 1 kg", "Bag size must be at least 1 kg",
@@ -315,6 +325,7 @@ async def feed_purchased(request: Request):
bags_count = int(bags_count_str) bags_count = int(bags_count_str)
except ValueError: except ValueError:
return _render_purchase_error( return _render_purchase_error(
request,
locations, locations,
feed_types, feed_types,
"Bags count must be a number", "Bags count must be a number",
@@ -322,6 +333,7 @@ async def feed_purchased(request: Request):
if bags_count < 1: if bags_count < 1:
return _render_purchase_error( return _render_purchase_error(
request,
locations, locations,
feed_types, feed_types,
"Bags count must be at least 1", "Bags count must be at least 1",
@@ -332,6 +344,7 @@ async def feed_purchased(request: Request):
bag_price_cents = int(bag_price_cents_str) bag_price_cents = int(bag_price_cents_str)
except ValueError: except ValueError:
return _render_purchase_error( return _render_purchase_error(
request,
locations, locations,
feed_types, feed_types,
"Price must be a number", "Price must be a number",
@@ -339,6 +352,7 @@ async def feed_purchased(request: Request):
if bag_price_cents < 0: if bag_price_cents < 0:
return _render_purchase_error( return _render_purchase_error(
request,
locations, locations,
feed_types, feed_types,
"Price cannot be negative", "Price cannot be negative",
@@ -379,6 +393,7 @@ async def feed_purchased(request: Request):
) )
except ValidationError as e: except ValidationError as e:
return _render_purchase_error( return _render_purchase_error(
request,
locations, locations,
feed_types, feed_types,
str(e), str(e),
@@ -390,7 +405,8 @@ async def feed_purchased(request: Request):
# Success: re-render form with fields cleared # Success: re-render form with fields cleared
response = HTMLResponse( response = HTMLResponse(
content=str( content=str(
page( render_page(
request,
feed_page( feed_page(
locations, locations,
feed_types, feed_types,
@@ -418,6 +434,7 @@ async def feed_purchased(request: Request):
def _render_give_error( def _render_give_error(
request,
locations, locations,
feed_types, feed_types,
error_message, error_message,
@@ -427,6 +444,7 @@ def _render_give_error(
"""Render give form with error message. """Render give form with error message.
Args: Args:
request: The Starlette request object.
locations: List of active locations. locations: List of active locations.
feed_types: List of active feed types. feed_types: List of active feed types.
error_message: Error message to display. error_message: Error message to display.
@@ -438,7 +456,8 @@ def _render_give_error(
""" """
return HTMLResponse( return HTMLResponse(
content=str( content=str(
page( render_page(
request,
feed_page( feed_page(
locations, locations,
feed_types, feed_types,
@@ -457,10 +476,11 @@ def _render_give_error(
) )
def _render_purchase_error(locations, feed_types, error_message): def _render_purchase_error(request, locations, feed_types, error_message):
"""Render purchase form with error message. """Render purchase form with error message.
Args: Args:
request: The Starlette request object.
locations: List of active locations. locations: List of active locations.
feed_types: List of active feed types. feed_types: List of active feed types.
error_message: Error message to display. error_message: Error message to display.
@@ -470,7 +490,8 @@ def _render_purchase_error(locations, feed_types, error_message):
""" """
return HTMLResponse( return HTMLResponse(
content=str( content=str(
page( render_page(
request,
feed_page( feed_page(
locations, locations,
feed_types, feed_types,

View File

@@ -18,7 +18,7 @@ from animaltrack.repositories.locations import LocationRepository
from animaltrack.services.location import LocationService, ValidationError from animaltrack.services.location import LocationService, ValidationError
from animaltrack.web.auth import require_role from animaltrack.web.auth import require_role
from animaltrack.web.responses import success_toast from animaltrack.web.responses import success_toast
from animaltrack.web.templates import page from animaltrack.web.templates import render_page
from animaltrack.web.templates.locations import location_list, rename_form from animaltrack.web.templates.locations import location_list, rename_form
# APIRouter for multi-file route organization # APIRouter for multi-file route organization
@@ -45,7 +45,8 @@ async def locations_index(req: Request):
db = req.app.state.db db = req.app.state.db
locations = LocationRepository(db).list_all() locations = LocationRepository(db).list_all()
return page( return render_page(
req,
location_list(locations), location_list(locations),
title="Locations - AnimalTrack", title="Locations - AnimalTrack",
active_nav=None, active_nav=None,

View File

@@ -21,7 +21,7 @@ from animaltrack.repositories.locations import LocationRepository
from animaltrack.selection import compute_roster_hash, parse_filter, resolve_filter from animaltrack.selection import compute_roster_hash, parse_filter, resolve_filter
from animaltrack.selection.validation import SelectionContext, validate_selection from animaltrack.selection.validation import SelectionContext, validate_selection
from animaltrack.services.animal import AnimalService, ValidationError from animaltrack.services.animal import AnimalService, ValidationError
from animaltrack.web.templates import page from animaltrack.web.templates import render_page
from animaltrack.web.templates.move import diff_panel, move_form from animaltrack.web.templates.move import diff_panel, move_form
@@ -111,7 +111,8 @@ def move_index(request: Request):
from_location_id, from_location_name = _get_from_location(db, resolved_ids, ts_utc) from_location_id, from_location_name = _get_from_location(db, resolved_ids, ts_utc)
roster_hash = compute_roster_hash(resolved_ids, from_location_id) roster_hash = compute_roster_hash(resolved_ids, from_location_id)
return page( return render_page(
request,
move_form( move_form(
locations, locations,
filter_str=filter_str, filter_str=filter_str,
@@ -153,16 +154,16 @@ async def animal_move(request: Request):
# Validation: destination required # Validation: destination required
if not to_location_id: if not to_location_id:
return _render_error_form(db, locations, filter_str, "Please select a destination") return _render_error_form(request, db, locations, filter_str, "Please select a destination")
# Validation: must have animals # Validation: must have animals
if not resolved_ids: if not resolved_ids:
return _render_error_form(db, locations, filter_str, "No animals selected to move") return _render_error_form(request, db, locations, filter_str, "No animals selected to move")
# Validation: destination must be different from source # Validation: destination must be different from source
if to_location_id == from_location_id: if to_location_id == from_location_id:
return _render_error_form( return _render_error_form(
db, locations, filter_str, "Destination must be different from source" request, db, locations, filter_str, "Destination must be different from source"
) )
# Validate destination exists and is active # Validate destination exists and is active
@@ -173,7 +174,9 @@ async def animal_move(request: Request):
break break
if not dest_location: if not dest_location:
return _render_error_form(db, locations, filter_str, "Invalid destination location") return _render_error_form(
request, db, locations, filter_str, "Invalid destination location"
)
# Build selection context for validation # Build selection context for validation
context = SelectionContext( context = SelectionContext(
@@ -192,7 +195,8 @@ async def animal_move(request: Request):
# Mismatch detected - return 409 with diff panel # Mismatch detected - return 409 with diff panel
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
diff_panel( diff_panel(
diff=result.diff, diff=result.diff,
filter_str=filter_str, filter_str=filter_str,
@@ -227,7 +231,9 @@ async def animal_move(request: Request):
# Check we still have animals to move after validation # Check we still have animals to move after validation
if not ids_to_move: if not ids_to_move:
return _render_error_form(db, locations, filter_str, "No animals remaining to move") return _render_error_form(
request, db, locations, filter_str, "No animals remaining to move"
)
# Create animal service # Create animal service
event_store = EventStore(db) event_store = EventStore(db)
@@ -255,12 +261,13 @@ async def animal_move(request: Request):
payload, ts_utc, actor, nonce=nonce, route="/actions/animal-move" payload, ts_utc, actor, nonce=nonce, route="/actions/animal-move"
) )
except ValidationError as e: except ValidationError as e:
return _render_error_form(db, locations, filter_str, str(e)) return _render_error_form(request, db, locations, filter_str, str(e))
# Success: re-render fresh form (nothing sticks per spec) # Success: re-render fresh form (nothing sticks per spec)
response = HTMLResponse( response = HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
move_form( move_form(
locations, locations,
action=animal_move, action=animal_move,
@@ -284,10 +291,11 @@ async def animal_move(request: Request):
return response return response
def _render_error_form(db, locations, filter_str, error_message): def _render_error_form(request, db, locations, filter_str, error_message):
"""Render form with error message. """Render form with error message.
Args: Args:
request: The Starlette request object.
db: Database connection. db: Database connection.
locations: List of active locations. locations: List of active locations.
filter_str: Current filter string. filter_str: Current filter string.
@@ -314,7 +322,8 @@ def _render_error_form(db, locations, filter_str, error_message):
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
move_form( move_form(
locations, locations,
filter_str=filter_str, filter_str=filter_str,

View File

@@ -16,7 +16,7 @@ from animaltrack.projections import EventLogProjection, ProjectionRegistry
from animaltrack.projections.products import ProductsProjection from animaltrack.projections.products import ProductsProjection
from animaltrack.repositories.products import ProductRepository from animaltrack.repositories.products import ProductRepository
from animaltrack.services.products import ProductService, ValidationError from animaltrack.services.products import ProductService, ValidationError
from animaltrack.web.templates import page from animaltrack.web.templates import render_page
from animaltrack.web.templates.products import product_sold_form from animaltrack.web.templates.products import product_sold_form
# APIRouter for multi-file route organization # APIRouter for multi-file route organization
@@ -70,25 +70,25 @@ async def product_sold(request: Request):
# Validate product_code # Validate product_code
if not product_code: if not product_code:
return _render_error_form(products, None, "Please select a product") return _render_error_form(request, products, None, "Please select a product")
# Validate quantity # Validate quantity
try: try:
quantity = int(quantity_str) quantity = int(quantity_str)
except ValueError: except ValueError:
return _render_error_form(products, product_code, "Quantity must be a number") return _render_error_form(request, products, product_code, "Quantity must be a number")
if quantity < 1: if quantity < 1:
return _render_error_form(products, product_code, "Quantity must be at least 1") return _render_error_form(request, products, product_code, "Quantity must be at least 1")
# Validate total_price_cents # Validate total_price_cents
try: try:
total_price_cents = int(total_price_str) total_price_cents = int(total_price_str)
except ValueError: except ValueError:
return _render_error_form(products, product_code, "Total price must be a number") return _render_error_form(request, products, product_code, "Total price must be a number")
if total_price_cents < 0: if total_price_cents < 0:
return _render_error_form(products, product_code, "Total price cannot be negative") return _render_error_form(request, products, product_code, "Total price cannot be negative")
# Get current timestamp # Get current timestamp
ts_utc = int(time.time() * 1000) ts_utc = int(time.time() * 1000)
@@ -124,12 +124,13 @@ async def product_sold(request: Request):
route="/actions/product-sold", route="/actions/product-sold",
) )
except ValidationError as e: except ValidationError as e:
return _render_error_form(products, product_code, str(e)) return _render_error_form(request, products, product_code, str(e))
# Success: re-render form with product sticking, other fields cleared # Success: re-render form with product sticking, other fields cleared
response = HTMLResponse( response = HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
product_sold_form( product_sold_form(
products, selected_product_code=product_code, action=product_sold products, selected_product_code=product_code, action=product_sold
), ),
@@ -147,10 +148,11 @@ async def product_sold(request: Request):
return response return response
def _render_error_form(products, selected_product_code, error_message): def _render_error_form(request, products, selected_product_code, error_message):
"""Render form with error message. """Render form with error message.
Args: Args:
request: The Starlette request object.
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.
@@ -160,7 +162,8 @@ def _render_error_form(products, selected_product_code, error_message):
""" """
return HTMLResponse( return HTMLResponse(
content=to_xml( content=to_xml(
page( render_page(
request,
product_sold_form( product_sold_form(
products, products,
selected_product_code=selected_product_code, selected_product_code=selected_product_code,

View File

@@ -8,7 +8,7 @@ from starlette.responses import HTMLResponse
from animaltrack.repositories.animals import AnimalRepository from animaltrack.repositories.animals import AnimalRepository
from animaltrack.repositories.locations import LocationRepository from animaltrack.repositories.locations import LocationRepository
from animaltrack.repositories.species import SpeciesRepository from animaltrack.repositories.species import SpeciesRepository
from animaltrack.web.templates.base import page from animaltrack.web.templates.base import render_page
from animaltrack.web.templates.registry import ( from animaltrack.web.templates.registry import (
animal_table_rows, animal_table_rows,
registry_page, registry_page,
@@ -56,7 +56,8 @@ def registry_index(request: Request):
return HTMLResponse(content=to_xml(tuple(rows))) return HTMLResponse(content=to_xml(tuple(rows)))
# Full page render # Full page render
return page( return render_page(
request,
registry_page( registry_page(
animals=result.items, animals=result.items,
facets=facets, facets=facets,

View File

@@ -1,7 +1,7 @@
# ABOUTME: Templates package for AnimalTrack web UI. # ABOUTME: Templates package for AnimalTrack web UI.
# ABOUTME: Contains reusable UI components built with FastHTML and MonsterUI. # ABOUTME: Contains reusable UI components built with FastHTML and MonsterUI.
from animaltrack.web.templates.base import page from animaltrack.web.templates.base import page, render_page
from animaltrack.web.templates.nav import BottomNav from animaltrack.web.templates.nav import BottomNav
__all__ = ["page", "BottomNav"] __all__ = ["page", "render_page", "BottomNav"]

View File

@@ -2,6 +2,7 @@
# ABOUTME: Provides consistent layout with MonsterUI theme and responsive nav. # ABOUTME: Provides consistent layout with MonsterUI theme and responsive nav.
from fasthtml.common import Container, Div, Script, Title from fasthtml.common import Container, Div, Script, Title
from starlette.requests import Request
from animaltrack.models.reference import UserRole from animaltrack.models.reference import UserRole
from animaltrack.web.templates.nav import BottomNav, BottomNavStyles from animaltrack.web.templates.nav import BottomNav, BottomNavStyles
@@ -123,3 +124,27 @@ def page(
# Mobile bottom nav # Mobile bottom nav
BottomNav(active_id=active_nav), BottomNav(active_id=active_nav),
) )
def render_page(request: Request, content, **page_kwargs):
"""Wrapper that auto-extracts auth from request for page().
Extracts username and user_role from request.scope["auth"] and
passes them to page(), eliminating the need to manually pass these
values in every route handler.
Args:
request: The Starlette request object.
content: Page content (FT components).
**page_kwargs: Additional arguments passed to page() (title, active_nav).
Returns:
Tuple of FT components for the complete page.
"""
auth = request.scope.get("auth")
return page(
content,
username=auth.username if auth else None,
user_role=auth.role if auth else None,
**page_kwargs,
)