V5.1 Modify Person table: split full name into first & last, added tags support
Quality Gate / gate (push) Failing after 11s

This commit is contained in:
Jim Lancaster
2026-08-23 12:23:57 -05:00
parent 141ee1fa85
commit ae3483ec2e
28 changed files with 555 additions and 145 deletions
+33 -9
View File
@@ -13,6 +13,7 @@ from transcription.db.models import DocumentPerson
from transcription.db.models import DocumentTag
from transcription.db.models import Job
from transcription.db.models import Person
from transcription.db.models import PersonTag
from transcription.db.models import Source
from transcription.db.models import Tag
from transcription.db.models import Photo
@@ -132,7 +133,7 @@ async def test_delete_document_removes_person_links(default_session_factory, tmp
name="person-linked-delete",
)
)
person = await people_service.create_person(Person(full_name="Linked Person"))
person = await people_service.create_person(Person(given_names="Linked", last_name="Person"))
author_role = await people_service.create_person_role(label="Author")
await people_service.create_document_person(
DocumentPerson(
@@ -197,7 +198,7 @@ async def test_read_person_detail_loads_document_links(default_session_factory):
name="linked-doc",
)
)
person = await people_service.create_person(Person(full_name="Linked Person"))
person = await people_service.create_person(Person(given_names="Linked", last_name="Person"))
author_role = await people_service.create_person_role(label="Author")
await people_service.create_document_person(
DocumentPerson(
@@ -221,16 +222,17 @@ async def test_update_person_refreshes_updated_timestamp(default_session_factory
created = await service.create_person(
Person(
full_name="timestamp-person",
given_names="timestamp",
last_name="person",
updated_at=datetime(2000, 1, 1, tzinfo=UTC),
)
)
original_updated_at = created.updated_at
created.display_name = "updated"
created.given_names = "updated"
updated = await service.update_person(created)
assert updated.display_name == "updated"
assert updated.given_names == "updated"
assert updated.updated_at >= original_updated_at
@@ -245,7 +247,7 @@ async def test_delete_person_removes_links_when_linked_documents_exist(default_s
name="block-person-delete-doc",
)
)
person = await service.create_person(Person(full_name="Blocked Person"))
person = await service.create_person(Person(given_names="Blocked", last_name="Person"))
author_role = await service.create_person_role(label="Author")
await service.create_document_person(
DocumentPerson(
@@ -268,7 +270,7 @@ async def test_delete_person_removes_links_when_linked_documents_exist(default_s
async def test_delete_person_succeeds_when_unlinked(default_session_factory):
service = PeopleService(session_factory=default_session_factory)
person = await service.create_person(Person(full_name="Free Person"))
person = await service.create_person(Person(given_names="Free", last_name="Person"))
await service.delete_person(person)
@@ -280,7 +282,7 @@ async def test_delete_person_succeeds_when_unlinked(default_session_factory):
async def test_delete_person_blocks_when_photos_exist(default_session_factory):
service = PeopleService(session_factory=default_session_factory)
person = await service.create_person(Person(full_name="Photo Protected Person"))
person = await service.create_person(Person(given_names="Photo Protected", last_name="Person"))
async with service._session_scope() as session:
session.add(
Photo(
@@ -313,7 +315,7 @@ async def test_update_document_person_changes_role_id(default_session_factory):
service = PeopleService(session_factory=default_session_factory)
document = await documents_service.create_document(Document(id=uuid4(), name="role-sync-doc"))
person = await service.create_person(Person(full_name="Role Sync Person"))
person = await service.create_person(Person(given_names="Role Sync", last_name="Person"))
author_role = await service.create_person_role(label="Author")
recipient_role = await service.create_person_role(label="Recipient")
link = await service.create_document_person(
@@ -357,3 +359,25 @@ async def test_sync_document_tags_by_labels_creates_and_replaces_tags(default_se
assert len(listed) == 1
listed_labels = {link.tag_ref.label for link in listed[0].document_tags if link.tag_ref is not None}
assert listed_labels == {"Census", "Research"}
@pytest.mark.asyncio
async def test_sync_person_tags_by_labels_creates_and_replaces_tags(default_session_factory):
people = PeopleService(session_factory=default_session_factory)
person = await people.create_person(Person(given_names="Tagged", last_name="Person"))
await people.sync_person_tags_by_labels(person_id=person.id, labels=["Family", "Census"])
await people.sync_person_tags_by_labels(person_id=person.id, labels=["Census", "Research"])
async with people._session_scope() as session:
links = (await session.exec(select(PersonTag).where(PersonTag.person_id == person.id))).all()
tags = (await session.exec(select(Tag))).all()
assert len(links) == 2
linked_ids = {link.tag_id for link in links}
linked_labels = {tag.label for tag in tags if tag.id in linked_ids}
assert linked_labels == {"Census", "Research"}
detail = await people.read_person_detail(person.id)
listed_labels = {link.tag_ref.label for link in detail.person_tags if link.tag_ref is not None}
assert listed_labels == {"Census", "Research"}
+2 -2
View File
@@ -18,7 +18,7 @@ async def test_create_photo_persists_media_and_primary_state(default_session_fac
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path / "uploads")
people = PeopleService(session_factory=default_session_factory)
photos = PhotosService(session_factory=default_session_factory, settings=settings)
person = await people.create_person(Person(full_name="Photo Person"))
person = await people.create_person(Person(given_names="Photo", last_name="Person"))
first = await photos.create_photo(person_id=person.id, filename="one.png", file_bytes=PNG_BYTES)
second = await photos.create_photo(person_id=person.id, filename="two.png", file_bytes=PNG_BYTES)
@@ -36,7 +36,7 @@ async def test_set_primary_and_delete_promotes_next_photo(default_session_factor
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path / "uploads")
people = PeopleService(session_factory=default_session_factory)
photos = PhotosService(session_factory=default_session_factory, settings=settings)
person = await people.create_person(Person(full_name="Primary Person"))
person = await people.create_person(Person(given_names="Primary", last_name="Person"))
first = await photos.create_photo(person_id=person.id, filename="one.png", file_bytes=PNG_BYTES)
second = await photos.create_photo(person_id=person.id, filename="two.png", file_bytes=PNG_BYTES)
+2 -2
View File
@@ -95,7 +95,7 @@ async def test_person_role_delete_allows_unreferenced_and_blocks_referenced(defa
unused = await people.create_person_role(label="Witness")
referenced = await people.create_person_role(label="Creator")
document = await documents.create_document(Document(name="Role document"))
person = await people.create_person(Person(full_name="Role Person"))
person = await people.create_person(Person(given_names="Role", last_name="Person"))
await people.create_document_person(
DocumentPerson(
document_id=document.id,
@@ -131,7 +131,7 @@ async def test_custom_person_role_can_be_used_for_document_link(default_session_
people = PeopleService(session_factory=default_session_factory)
role = await people.create_person_role(label="Witness")
document = await documents.create_document(Document(name="Witnessed document"))
person = await people.create_person(Person(full_name="Archive Witness"))
person = await people.create_person(Person(given_names="Archive", last_name="Witness"))
link = await people.add_document_person_link(
document_id=document.id,
+3 -2
View File
@@ -34,10 +34,11 @@ async def test_document_update_advances_updated_at(default_session_factory):
@pytest.mark.asyncio
async def test_person_update_advances_updated_at(default_session_factory):
people = PeopleService(session_factory=default_session_factory)
person = await people.create_person(Person(full_name="Grace Hopper"))
person = await people.create_person(Person(given_names="Grace", last_name="Hopper"))
original = person.updated_at
person.full_name = "Rear Adm. Grace Hopper"
person.given_names = "Rear Adm. Grace"
person.last_name = "Hopper"
updated = await people.update_person(person)
assert updated.updated_at > original
+5 -5
View File
@@ -28,7 +28,7 @@ async def test_people_service_handles_person_and_document_person_crud(default_se
people_service = PeopleService(session_factory=default_session_factory)
document = await documents.create_document(Document(id=uuid4(), name="person-doc"))
person = await people_service.create_person(Person(full_name="Ada Lovelace"))
person = await people_service.create_person(Person(given_names="Ada", last_name="Lovelace"))
author_role = await people_service.create_person_role(label="Author")
recipient_role = await people_service.create_person_role(label="Recipient")
@@ -61,15 +61,15 @@ async def test_people_service_handles_person_and_document_person_crud(default_se
async def test_people_service_normalizes_and_rejects_duplicate_family_search_ids(default_session_factory):
people_service = PeopleService(session_factory=default_session_factory)
created = await people_service.create_person(Person(full_name="Hig Higgins", family_search_id=" g8t4-mdq "))
created = await people_service.create_person(Person(given_names="Hig", last_name="Higgins", family_search_id=" g8t4-mdq "))
assert created.family_search_id == "G8T4-MDQ"
with pytest.raises(PeopleError) as duplicate:
await people_service.create_person(Person(full_name="Duplicate Hig", family_search_id="G8T4-MDQ"))
await people_service.create_person(Person(given_names="Duplicate", last_name="Hig", family_search_id="G8T4-MDQ"))
assert duplicate.value.category == ErrorCategory.CONFLICT
with pytest.raises(PeopleError) as malformed:
await people_service.create_person(Person(full_name="Malformed", family_search_id="not-an-id"))
await people_service.create_person(Person(given_names="Malformed", last_name="Person", family_search_id="not-an-id"))
assert malformed.value.category == ErrorCategory.VALIDATION
@@ -228,7 +228,7 @@ async def test_document_detail_loads_linked_person_relationship(default_session_
people_service = PeopleService(session_factory=default_session_factory)
document = await documents.create_document(Document(id=uuid4(), name="detail-person-doc"))
person = await people_service.create_person(Person(full_name="Grace Hopper"))
person = await people_service.create_person(Person(given_names="Grace", last_name="Hopper"))
author_role = await people_service.create_person_role(label="Author")
await people_service.create_document_person(
DocumentPerson(
+3 -3
View File
@@ -45,7 +45,7 @@ async def test_update_document_with_people_rolls_back_document_and_links(default
people = PeopleService(session_factory=default_session_factory)
role = await people.create_person_role(label="Witness")
inactive = await people.create_person_role(label="Former Witness", is_active=False)
person = await people.create_person(Person(full_name="Archive Witness"))
person = await people.create_person(Person(given_names="Archive", last_name="Witness"))
document = await create_document_with_people(
document=Document(name="Original name"),
links=[DocumentPersonInput(person_id=person.id, role_id=role.id)],
@@ -83,7 +83,7 @@ async def test_direct_link_writes_reject_new_inactive_role_assignments(default_s
people = PeopleService(session_factory=default_session_factory)
active = await people.create_person_role(label="Witness")
inactive = await people.create_person_role(label="Former Witness", is_active=False)
person = await people.create_person(Person(full_name="Archive Witness"))
person = await people.create_person(Person(given_names="Archive", last_name="Witness"))
document = await documents.create_document(Document(name="Role rules"))
link = await people.add_document_person_link(
document_id=document.id,
@@ -114,7 +114,7 @@ async def test_document_print_projection_uses_semantic_author_and_current_text(d
document = await documents.create_document(
Document(name="Print Me", notes="Archive note", document_type_id=document_type.id)
)
person = await people.create_person(Person(full_name="Historic Author"))
person = await people.create_person(Person(given_names="Historic", last_name="Author"))
async with people._session_scope() as session:
author = PersonRole(