From e86af247da959311c9190ffa911df13c2c4a21a4 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Sat, 3 Jan 2026 09:10:32 +0000 Subject: [PATCH] fix: use sentinel value for optional brood location dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/animaltrack/web/routes/actions.py | 6 ++++- src/animaltrack/web/templates/actions.py | 4 +++- tests/test_web_actions.py | 28 ++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/animaltrack/web/routes/actions.py b/src/animaltrack/web/routes/actions.py index 1548d07..f87a346 100644 --- a/src/animaltrack/web/routes/actions.py +++ b/src/animaltrack/web/routes/actions.py @@ -278,7 +278,11 @@ async def hatch_recorded(request: Request, session): # Extract form data species = form.get("species", "") 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") notes = form.get("notes", "") or None nonce = form.get("nonce") diff --git a/src/animaltrack/web/templates/actions.py b/src/animaltrack/web/templates/actions.py index dd1bcae..9b50d3d 100644 --- a/src/animaltrack/web/templates/actions.py +++ b/src/animaltrack/web/templates/actions.py @@ -327,10 +327,12 @@ def hatch_form( ) # 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 = [ Option( "Same as hatch location", - value="", + value="__none__", selected=not selected_brood_location, ) ] diff --git a/tests/test_web_actions.py b/tests/test_web_actions.py index ffc9d84..4e1ae42 100644 --- a/tests/test_web_actions.py +++ b/tests/test_web_actions.py @@ -362,6 +362,34 @@ class TestHatchRecordingSuccess: 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): """Successful hatch recording renders toast in response body.""" resp = client.post(