Feature 03: Children Registry and Admin Allowlist -> dev #56

Merged
2 changed files with 65 additions and 14 deletions
Showing only changes of commit 712527d5ba - Show all commits
+15 -6
View File
@@ -74,16 +74,25 @@ def _validate_sort_order(value: object) -> int:
async def _warn_on_duplicate_name(
database: NestQuestDatabase, display_name: str
database: NestQuestDatabase,
display_name: str,
*,
exclude_child_id: int | None = None,
) -> None:
"""Log a warning when ``display_name`` duplicates an existing child.
Case-insensitive and whitespace-normalised on both sides. The
duplicate is still allowed — this only makes it visible; the log
line names both rows so an accidental double-add can be found.
``exclude_child_id`` removes the child being edited from the scan,
so an edit only warns when the result collides with a DIFFERENT
child's name — including case-only edits, where the self-comparison
would otherwise hide a real duplicate.
"""
lowered = display_name.casefold()
for child in await ChildrenDao(database).list_all():
if exclude_child_id is not None and child.id == exclude_child_id:
continue
if child.display_name.strip().casefold() == lowered:
LOGGER.warning(
"Child display name %r duplicates existing child %d "
@@ -142,9 +151,10 @@ async def edit_child(
field raises: the DAO's update writes only provided fields and
cannot express NULL, so a silent no-op would mask the caller's
intent (see the module docstring). A provided display name is
validated like create's and, when it duplicates a DIFFERENT child,
still allowed with a warning. Raises ValueError when the child
does not exist.
validated like create's; the duplicate scan excludes only the child
being edited, so a case-only edit colliding with another child's
normalised name still warns. Raises ValueError when the child does
not exist.
"""
dao = ChildrenDao(database)
existing = await dao.get(child_id)
@@ -154,8 +164,7 @@ async def edit_child(
if display_name is not _UNSET:
name = _validate_text(display_name, "display_name", required=True)
assert name is not None
if name.casefold() != existing.display_name.strip().casefold():
await _warn_on_duplicate_name(database, name)
await _warn_on_duplicate_name(database, name, exclude_child_id=child_id)
updates["display_name"] = name
if colour is not _UNSET:
value = _validate_text(colour, "colour")
+50 -8
View File
@@ -291,21 +291,63 @@ def test_edit_child_rename_to_duplicate_warns_but_different_name_not(
# Renaming Bo onto Ada's name warns (different child's name).
edited = await edit_child(database, bo.id, display_name="ada")
assert edited.display_name == "ada"
assert any(
"duplicates existing child" in r.getMessage()
for r in caplog.records
)
warnings = [
r for r in caplog.records if "duplicates existing child" in r.getMessage()
]
assert len(warnings) == 1
assert "Ada" in warnings[0].getMessage()
return edited
_with_db(tmp_path, "edit-rename-duplicate.db")(_body)
def test_edit_child_rename_to_own_name_casing_alone_warns_not(
tmp_path, caplog
) -> None:
"""Renaming a child to their own name (casing aside) is not a
duplicate when NO OTHER child carries the normalised name."""
async def _body(database):
ada = await create_child(database, "Ada")
caplog.clear()
# Renaming a child to their own name (casing aside) is not a
# duplicate: no warning.
await edit_child(database, ada.id, display_name="ADA")
edited = await edit_child(database, ada.id, display_name="ADA")
assert edited.display_name == "ADA"
assert not any(
"duplicates existing child" in r.getMessage()
for r in caplog.records
)
return edited
_with_db(tmp_path, "edit-rename-self.db")(_body)
def test_edit_child_case_only_edit_colliding_with_other_child_warns(
tmp_path, caplog
) -> None:
"""A case-only edit must not hide a real duplicate: with two Ada
profiles already present, editing either one's name to another
casing still warns (the scan excludes only the edited child)."""
async def _body(database):
first = await create_child(database, "Ada")
await create_child(database, "ada")
caplog.clear()
edited = await edit_child(database, first.id, display_name="ADA")
assert edited.display_name == "ADA"
warnings = [
r for r in caplog.records if "duplicates existing child" in r.getMessage()
]
assert len(warnings) == 1
# Editing the name to a value that STAYS duplicated (no change,
# other child still carries it) warns too: the scan is on the
# resulting state, not on whether the word moved.
caplog.clear()
await edit_child(database, first.id, display_name=" ada ")
warnings = [
r for r in caplog.records if "duplicates existing child" in r.getMessage()
]
assert len(warnings) == 1
return None
_with_db(tmp_path, "edit-rename-duplicate.db")(_body)
_with_db(tmp_path, "edit-case-only-duplicate.db")(_body)
# ---------------------------------------------------------------------------