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:
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user