Fix FastHTML empty value attribute omission in select options

FastHTML omits the value attribute when value="" (empty string), causing
browsers to use the option's text content as the submitted value. This
made forms send "Keep current" or "No change" text instead of empty
string, failing Pydantic enum validation.

Fixed by using "-" as a sentinel value instead of "" for "no change"
options, and updating route handlers to treat "-" as None.

Affected forms:
- Promote form (sex, repro_status)
- Update attributes form (sex, life_stage, repro_status)
- Outcome form (yield_product_code)

🤖 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-08 09:31:11 +00:00
parent 29fbe68c73
commit fb59ef72a8
2 changed files with 27 additions and 12 deletions

View File

@@ -426,10 +426,13 @@ async def animal_promote(request: Request):
form = await request.form()
# Extract form data
# Note: "-" is used as sentinel for "no change" because FastHTML omits empty value attributes
animal_id = form.get("animal_id", "")
nickname = form.get("nickname", "") or None
sex = form.get("sex", "") or None
repro_status = form.get("repro_status", "") or None
sex_raw = form.get("sex", "")
sex = None if sex_raw in ("", "-") else sex_raw
repro_raw = form.get("repro_status", "")
repro_status = None if repro_raw in ("", "-") else repro_raw
distinguishing_traits = form.get("distinguishing_traits", "") or None
notes = form.get("notes", "") or None
nonce = form.get("nonce")
@@ -1034,10 +1037,14 @@ async def animal_attrs(request: Request, session):
form = await request.form()
# Extract form data
# Note: "-" is used as sentinel for "no change" because FastHTML omits empty value attributes
filter_str = form.get("filter", "")
sex = form.get("sex", "").strip() or None
life_stage = form.get("life_stage", "").strip() or None
repro_status = form.get("repro_status", "").strip() or None
sex_raw = form.get("sex", "").strip()
sex = None if sex_raw in ("", "-") else sex_raw
life_stage_raw = form.get("life_stage", "").strip()
life_stage = None if life_stage_raw in ("", "-") else life_stage_raw
repro_raw = form.get("repro_status", "").strip()
repro_status = None if repro_raw in ("", "-") else repro_raw
roster_hash = form.get("roster_hash", "")
confirmed = form.get("confirmed", "") == "true"
nonce = form.get("nonce")
@@ -1277,7 +1284,9 @@ async def animal_outcome(request: Request, session):
nonce = form.get("nonce")
# Yield item fields
yield_product_code = form.get("yield_product_code", "").strip() or None
# Note: "-" is used as sentinel for "no selection" because FastHTML omits empty value attributes
yield_product_raw = form.get("yield_product_code", "").strip()
yield_product_code = None if yield_product_raw in ("", "-") else yield_product_raw
yield_unit = form.get("yield_unit", "").strip() or None
yield_quantity_str = form.get("yield_quantity", "").strip()
yield_weight_str = form.get("yield_weight_kg", "").strip()

View File

@@ -443,8 +443,9 @@ def promote_form(
action_text = "Renaming" if is_rename else "Promoting"
# Build sex options (optional - can refine current value)
# Note: Use "-" sentinel instead of "" because FastHTML omits empty value attributes
sexes = [
("", "Keep current"),
("-", "Keep current"),
("female", "Female"),
("male", "Male"),
]
@@ -453,8 +454,9 @@ def promote_form(
sex_options.append(Option(label, value=code, selected=code == selected_sex))
# Build repro status options (optional) - must match ReproStatus enum
# Note: Use "-" sentinel instead of "" because FastHTML omits empty value attributes
repro_statuses = [
("", "Keep current"),
("-", "Keep current"),
("intact", "Intact"),
("wether", "Wether (castrated male)"),
("spayed", "Spayed (female)"),
@@ -1032,24 +1034,27 @@ def attrs_form(
)
# Build sex options
# Note: Use "-" sentinel instead of "" because FastHTML omits empty value attributes
sex_options = [
Option("No change", value="", selected=True),
Option("No change", value="-", selected=True),
Option("Female", value="female"),
Option("Male", value="male"),
Option("Unknown", value="unknown"),
]
# Build life stage options
# Note: Use "-" sentinel instead of "" because FastHTML omits empty value attributes
life_stage_options = [
Option("No change", value="", selected=True),
Option("No change", value="-", selected=True),
Option("Hatchling", value="hatchling"),
Option("Juvenile", value="juvenile"),
Option("Adult", value="adult"),
]
# Build repro status options (intact, wether, spayed, unknown)
# Note: Use "-" sentinel instead of "" because FastHTML omits empty value attributes
repro_status_options = [
Option("No change", value="", selected=True),
Option("No change", value="-", selected=True),
Option("Intact", value="intact"),
Option("Wether (castrated male)", value="wether"),
Option("Spayed (female)", value="spayed"),
@@ -1293,7 +1298,8 @@ def outcome_form(
]
# Build product options for yield items
product_options = [Option("Select product...", value="", selected=True)]
# Note: Use "-" sentinel instead of "" because FastHTML omits empty value attributes
product_options = [Option("Select product...", value="-", selected=True)]
for code, name in products:
product_options.append(Option(f"{name} ({code})", value=code))