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