fix: use sentinel value for optional brood location dropdown

FastHTML omits empty string attributes (value=""), causing browsers to
submit the option's text content "Same as hatch location" instead of an
empty value. This resulted in a ULID validation error.

Use "__none__" as a sentinel value that the server converts to None.

🤖 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-03 09:10:32 +00:00
parent 9fbda655f5
commit e86af247da
3 changed files with 36 additions and 2 deletions

View File

@@ -278,7 +278,11 @@ async def hatch_recorded(request: Request, session):
# Extract form data # Extract form data
species = form.get("species", "") species = form.get("species", "")
location_id = form.get("location_id", "") location_id = form.get("location_id", "")
assigned_brood_location_id = form.get("assigned_brood_location_id", "") or None # "__none__" is a sentinel value used because FastHTML omits empty string attributes
brood_location_raw = form.get("assigned_brood_location_id", "")
assigned_brood_location_id = (
None if brood_location_raw in ("", "__none__") else brood_location_raw
)
hatched_live_str = form.get("hatched_live", "0") hatched_live_str = form.get("hatched_live", "0")
notes = form.get("notes", "") or None notes = form.get("notes", "") or None
nonce = form.get("nonce") nonce = form.get("nonce")

View File

@@ -327,10 +327,12 @@ def hatch_form(
) )
# Build brood location options (optional) # Build brood location options (optional)
# Note: We use "__none__" as a sentinel value instead of "" because FastHTML
# omits empty string attributes, causing browsers to submit the text content.
brood_location_options = [ brood_location_options = [
Option( Option(
"Same as hatch location", "Same as hatch location",
value="", value="__none__",
selected=not selected_brood_location, selected=not selected_brood_location,
) )
] ]

View File

@@ -362,6 +362,34 @@ class TestHatchRecordingSuccess:
assert count_at_nursery >= 3 assert count_at_nursery >= 3
def test_hatch_with_sentinel_brood_location_value(self, client, seeded_db, location_strip1_id):
"""POST with __none__ sentinel value for brood location works correctly.
The form uses "__none__" as a sentinel value because FastHTML omits empty
string attributes, which would cause browsers to submit the option text
content instead.
"""
resp = client.post(
"/actions/hatch-recorded",
data={
"species": "duck",
"location_id": location_strip1_id,
"assigned_brood_location_id": "__none__",
"hatched_live": "2",
"nonce": "test-hatch-nonce-sentinel",
},
)
assert resp.status_code == 200
# Verify hatchlings are at hatch location (not a separate brood location)
count_at_location = seeded_db.execute(
"SELECT COUNT(*) FROM animal_registry WHERE location_id = ? AND life_stage = 'hatchling'",
(location_strip1_id,),
).fetchone()[0]
assert count_at_location >= 2
def test_hatch_success_returns_toast(self, client, seeded_db, location_strip1_id): def test_hatch_success_returns_toast(self, client, seeded_db, location_strip1_id):
"""Successful hatch recording renders toast in response body.""" """Successful hatch recording renders toast in response body."""
resp = client.post( resp = client.post(