Fix CSRF 403, improve registry UI, add phonetic IDs

- Fix CSRF token handling for production: generate tokens with HMAC,
  set cookie via afterware, inject into HTMX requests via JS
- Improve registry page: filter at top with better proportions,
  compact horizontal pill layout for facets
- Add phonetic ID encoding (e.g., "tobi-kafu-meli") for animal display
  instead of truncated ULIDs
- Remove "subadult" life stage (migration converts to juvenile)
- Change "Death (natural)" outcome label to just "Death"
- Show sex/life stage in animal picker alongside species/location

🤖 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-05 15:20:26 +00:00
parent a4b4fe6ab8
commit 14bf2fa4ae
16 changed files with 405 additions and 64 deletions

View File

@@ -1,7 +1,7 @@
# ABOUTME: Tests for ULID generation utility.
# ABOUTME: Verifies that generated IDs are valid 26-character ULIDs and unique.
# ABOUTME: Tests for ULID generation and phonetic encoding utilities.
# ABOUTME: Verifies ULID format, uniqueness, and phonetic display encoding.
from animaltrack.id_gen import generate_id
from animaltrack.id_gen import format_animal_id, generate_id, ulid_to_phonetic
class TestGenerateId:
@@ -28,3 +28,74 @@ class TestGenerateId:
# Crockford base32 excludes I, L, O, U
valid_chars = set("0123456789ABCDEFGHJKMNPQRSTVWXYZ")
assert all(c in valid_chars for c in result)
class TestUlidToPhonetic:
"""Tests for the ulid_to_phonetic function."""
def test_returns_hyphenated_syllables(self):
"""Result is hyphen-separated syllables."""
ulid = generate_id()
result = ulid_to_phonetic(ulid)
parts = result.split("-")
assert len(parts) == 4 # Default syllable count
def test_syllables_are_cv_format(self):
"""Each syllable is consonant-vowel format."""
ulid = generate_id()
result = ulid_to_phonetic(ulid)
consonants = set("bdfgklmnprstvwxz")
vowels = set("aeiou")
for syllable in result.split("-"):
assert len(syllable) == 2
assert syllable[0] in consonants
assert syllable[1] in vowels
def test_same_ulid_produces_same_phonetic(self):
"""Same ULID always produces the same phonetic."""
ulid = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
result1 = ulid_to_phonetic(ulid)
result2 = ulid_to_phonetic(ulid)
assert result1 == result2
def test_different_ulids_produce_different_phonetics(self):
"""Different ULIDs produce different phonetics (with high probability)."""
ulids = [generate_id() for _ in range(100)]
phonetics = [ulid_to_phonetic(u) for u in ulids]
# All should be unique
assert len(set(phonetics)) == 100
def test_custom_syllable_count(self):
"""Can specify custom number of syllables."""
ulid = generate_id()
result = ulid_to_phonetic(ulid, syllable_count=3)
assert len(result.split("-")) == 3
result = ulid_to_phonetic(ulid, syllable_count=5)
assert len(result.split("-")) == 5
class TestFormatAnimalId:
"""Tests for the format_animal_id function."""
def test_returns_nickname_when_provided(self):
"""Returns nickname if provided."""
ulid = generate_id()
result = format_animal_id(ulid, nickname="Daisy")
assert result == "Daisy"
def test_returns_phonetic_when_no_nickname(self):
"""Returns phonetic encoding when nickname is None."""
ulid = generate_id()
result = format_animal_id(ulid, nickname=None)
# Should be phonetic format (4 syllables, hyphen-separated)
parts = result.split("-")
assert len(parts) == 4
def test_returns_phonetic_when_nickname_empty(self):
"""Returns phonetic encoding when nickname is empty string."""
ulid = generate_id()
# Empty string is falsy, should use phonetic
result = format_animal_id(ulid, nickname="")
parts = result.split("-")
assert len(parts) == 4