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