generated from john/python-template
V4.4 Complete
This commit is contained in:
@@ -5,15 +5,12 @@ from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from sqlmodel import select
|
||||
|
||||
from transcription.config import Settings
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import DocumentPerson
|
||||
from transcription.db.models import DocumentPersonRole
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import Person
|
||||
from transcription.db.models import PersonRole
|
||||
from transcription.db.models import Source
|
||||
from transcription.services.documents import DocumentDeleteBlockedError
|
||||
from transcription.services.documents import DocumentError
|
||||
@@ -132,11 +129,12 @@ async def test_delete_document_removes_person_links(default_session_factory, tmp
|
||||
)
|
||||
)
|
||||
person = await people_service.create_person(Person(full_name="Linked Person"))
|
||||
author_role = await people_service.create_person_role(label="Author")
|
||||
await people_service.create_document_person(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
role_id=author_role.id,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -144,7 +142,7 @@ async def test_delete_document_removes_person_links(default_session_factory, tmp
|
||||
assert len(links_before_delete) == 1
|
||||
assert links_before_delete[0].role_id is not None
|
||||
assert links_before_delete[0].role_ref is not None
|
||||
assert links_before_delete[0].role_ref.code == "author"
|
||||
assert links_before_delete[0].role_ref.label == "Author"
|
||||
|
||||
document_dir = service.settings.upload_dir / "documents" / str(document.id)
|
||||
document_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -196,11 +194,12 @@ async def test_read_person_detail_loads_document_links(default_session_factory):
|
||||
)
|
||||
)
|
||||
person = await people_service.create_person(Person(full_name="Linked Person"))
|
||||
author_role = await people_service.create_person_role(label="Author")
|
||||
await people_service.create_document_person(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
role_id=author_role.id,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -243,11 +242,12 @@ async def test_delete_person_removes_links_when_linked_documents_exist(default_s
|
||||
)
|
||||
)
|
||||
person = await service.create_person(Person(full_name="Blocked Person"))
|
||||
author_role = await service.create_person_role(label="Author")
|
||||
await service.create_document_person(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
role_id=author_role.id,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -285,19 +285,19 @@ async def test_create_document_uses_existing_document_type_registry(default_sess
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_document_person_sets_role_id_from_legacy_role(default_session_factory):
|
||||
async def test_update_document_person_changes_role_id(default_session_factory):
|
||||
documents_service = DocumentService(session_factory=default_session_factory)
|
||||
service = PeopleService(session_factory=default_session_factory)
|
||||
|
||||
document = await documents_service.create_document(
|
||||
Document(id=uuid4(), name="role-sync-doc", document_type="letter")
|
||||
)
|
||||
document = await documents_service.create_document(Document(id=uuid4(), name="role-sync-doc"))
|
||||
person = await service.create_person(Person(full_name="Role Sync 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(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
role_id=author_role.id,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -306,15 +306,8 @@ async def test_update_document_person_sets_role_id_from_legacy_role(default_sess
|
||||
id=link.id,
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.RECIPIENT,
|
||||
role_id=None,
|
||||
role_id=recipient_role.id,
|
||||
)
|
||||
)
|
||||
|
||||
assert updated.role == DocumentPersonRole.RECIPIENT
|
||||
assert updated.role_id is not None
|
||||
|
||||
async with service._session_scope() as session:
|
||||
recipient_role = (await session.exec(select(PersonRole).where(PersonRole.code == "recipient"))).first()
|
||||
assert recipient_role is not None
|
||||
assert updated.role_id == recipient_role.id
|
||||
|
||||
@@ -6,8 +6,8 @@ import pytest
|
||||
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import DocumentPerson
|
||||
from transcription.db.models import DocumentPersonRole
|
||||
from transcription.db.models import Person
|
||||
from transcription.db.models import PersonRole
|
||||
from transcription.errors import ErrorCategory
|
||||
from transcription.services.documents import DocumentService
|
||||
from transcription.services.documents import DocumentTypeError
|
||||
@@ -73,24 +73,27 @@ async def test_document_type_delete_allows_unreferenced_and_blocks_referenced(de
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_person_role_maintenance_orders_by_label_then_code(default_session_factory):
|
||||
async def test_person_role_maintenance_orders_by_normalized_label(default_session_factory):
|
||||
service = PeopleService(session_factory=default_session_factory)
|
||||
second = await service.create_person_role(code="witness", label="Witness")
|
||||
first = await service.create_person_role(code="author", label="Author")
|
||||
second = await service.create_person_role(label="Witness")
|
||||
first = await service.create_person_role(label="Archivist")
|
||||
|
||||
updated = await service.update_person_role(second.id, label="Attestor", is_active=False)
|
||||
|
||||
assert updated.code == "witness"
|
||||
assert [item.id for item in await service.list_person_roles(active_only=False)] == [second.id, first.id]
|
||||
assert updated.semantic_key is None
|
||||
assert [item.id for item in await service.list_person_roles(active_only=False)] == [first.id, second.id]
|
||||
assert [item.id for item in await service.list_person_roles()] == [first.id]
|
||||
summaries = {item.id: item for item in await service.list_person_role_summaries()}
|
||||
assert summaries[second.id].link_count == 0
|
||||
assert summaries[second.id].is_built_in is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_person_role_delete_allows_unreferenced_and_blocks_referenced(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
people = PeopleService(session_factory=default_session_factory)
|
||||
unused = await people.create_person_role(code="witness", label="Witness")
|
||||
referenced = await people.create_person_role(code="author", label="Author")
|
||||
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"))
|
||||
await people.create_document_person(
|
||||
@@ -98,7 +101,6 @@ async def test_person_role_delete_allows_unreferenced_and_blocks_referenced(defa
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role_id=referenced.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -113,12 +115,12 @@ async def test_person_role_delete_allows_unreferenced_and_blocks_referenced(defa
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_person_role_duplicate_code_is_conflict(default_session_factory):
|
||||
async def test_person_role_duplicate_normalized_label_is_conflict(default_session_factory):
|
||||
service = PeopleService(session_factory=default_session_factory)
|
||||
await service.create_person_role(code="author", label="Author")
|
||||
await service.create_person_role(label="Witness")
|
||||
|
||||
with pytest.raises(PersonRoleError) as caught:
|
||||
await service.create_person_role(code=" AUTHOR ", label="Duplicate")
|
||||
await service.create_person_role(label=" witness ")
|
||||
|
||||
assert caught.value.category == ErrorCategory.CONFLICT
|
||||
|
||||
@@ -127,7 +129,7 @@ async def test_person_role_duplicate_code_is_conflict(default_session_factory):
|
||||
async def test_custom_person_role_can_be_used_for_document_link(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
people = PeopleService(session_factory=default_session_factory)
|
||||
role = await people.create_person_role(code="witness", label="Witness")
|
||||
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"))
|
||||
|
||||
@@ -138,30 +140,24 @@ async def test_custom_person_role_can_be_used_for_document_link(default_session_
|
||||
)
|
||||
loaded = await people.list_document_people(document_id=document.id)
|
||||
|
||||
assert link.role == "witness"
|
||||
assert link.role_id == role.id
|
||||
assert loaded[0].role_ref is not None
|
||||
assert loaded[0].role_ref.code == "witness"
|
||||
assert loaded[0].role_ref.label == "Witness"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_person_role_delete_blocks_legacy_only_reference(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
async def test_built_in_person_role_cannot_be_deleted(default_session_factory):
|
||||
people = PeopleService(session_factory=default_session_factory)
|
||||
role = await people.create_person_role(code="witness", label="Witness")
|
||||
document = await documents.create_document(Document(name="Legacy role document"))
|
||||
person = await people.create_person(Person(full_name="Legacy Witness"))
|
||||
async with people._session_scope() as session:
|
||||
session.add(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role="witness",
|
||||
role_id=None,
|
||||
)
|
||||
role = PersonRole(
|
||||
semantic_key="author",
|
||||
label="Author",
|
||||
normalized_label="author",
|
||||
)
|
||||
session.add(role)
|
||||
await session.commit()
|
||||
await session.refresh(role)
|
||||
|
||||
assert await people.is_person_role_referenced(role.id) is True
|
||||
with pytest.raises(PersonRoleError) as caught:
|
||||
await people.delete_person_role(role.id)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import pytest
|
||||
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import DocumentPerson
|
||||
from transcription.db.models import DocumentPersonRole
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import JobSource
|
||||
from transcription.db.models import JobSourceStatus
|
||||
@@ -27,23 +26,23 @@ async def test_people_service_handles_person_and_document_person_crud(default_se
|
||||
|
||||
document = await documents.create_document(Document(id=uuid4(), name="person-doc"))
|
||||
person = await people_service.create_person(Person(full_name="Ada Lovelace"))
|
||||
author_role = await people_service.create_person_role(label="Author")
|
||||
recipient_role = await people_service.create_person_role(label="Recipient")
|
||||
|
||||
assert document.document_type_id is None
|
||||
|
||||
link = await people_service.create_document_person(
|
||||
DocumentPerson(document_id=document.id, person_id=person.id, role=DocumentPersonRole.AUTHOR)
|
||||
DocumentPerson(document_id=document.id, person_id=person.id, role_id=author_role.id)
|
||||
)
|
||||
|
||||
fetched = await people_service.read_document_person(link.id)
|
||||
assert fetched.id == link.id
|
||||
assert fetched.role == DocumentPersonRole.AUTHOR
|
||||
assert fetched.role_id is not None
|
||||
assert fetched.role_id == author_role.id
|
||||
|
||||
updated_link = await people_service.update_document_person(
|
||||
DocumentPerson(id=link.id, document_id=document.id, person_id=person.id, role=DocumentPersonRole.RECIPIENT)
|
||||
DocumentPerson(id=link.id, document_id=document.id, person_id=person.id, role_id=recipient_role.id)
|
||||
)
|
||||
assert updated_link.role == DocumentPersonRole.RECIPIENT
|
||||
assert updated_link.role_id is not None
|
||||
assert updated_link.role_id == recipient_role.id
|
||||
|
||||
listed = await people_service.list_document_people(document_id=document.id)
|
||||
assert len(listed) == 1
|
||||
@@ -59,21 +58,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(full_name="Hig 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(full_name="Duplicate 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(full_name="Malformed", family_search_id="not-an-id"))
|
||||
assert malformed.value.category == ErrorCategory.VALIDATION
|
||||
|
||||
|
||||
@@ -202,11 +195,12 @@ async def test_document_detail_loads_linked_person_relationship(default_session_
|
||||
|
||||
document = await documents.create_document(Document(id=uuid4(), name="detail-person-doc"))
|
||||
person = await people_service.create_person(Person(full_name="Grace Hopper"))
|
||||
author_role = await people_service.create_person_role(label="Author")
|
||||
await people_service.create_document_person(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
role_id=author_role.id,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -216,7 +210,7 @@ async def test_document_detail_loads_linked_person_relationship(default_session_
|
||||
link = detail.document_people[0]
|
||||
assert link.person is not None
|
||||
assert link.person.full_name == "Grace Hopper"
|
||||
assert link.role == DocumentPersonRole.AUTHOR
|
||||
assert link.role_id == author_role.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import DocumentPerson
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import JobStatus
|
||||
from transcription.db.models import Person
|
||||
from transcription.db.models import PersonRole
|
||||
from transcription.db.models import Source
|
||||
from transcription.services.documents import DocumentService
|
||||
from transcription.services.jobs import JobService
|
||||
from transcription.services.people import DocumentPersonInput
|
||||
from transcription.services.people import PeopleError
|
||||
from transcription.services.people import PeopleService
|
||||
from transcription.services.sources import SourceService
|
||||
from transcription.services.workflows import create_document_with_people
|
||||
from transcription.services.workflows import update_document_with_people
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_document_with_people_rolls_back_on_invalid_person(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
people = PeopleService(session_factory=default_session_factory)
|
||||
role = await people.create_person_role(label="Witness")
|
||||
|
||||
with pytest.raises(PeopleError):
|
||||
await create_document_with_people(
|
||||
document=Document(name="Must roll back"),
|
||||
links=[DocumentPersonInput(person_id=uuid4(), role_id=role.id)],
|
||||
documents=documents,
|
||||
people=people,
|
||||
)
|
||||
|
||||
assert await documents.query_documents(name="Must roll back") == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_document_with_people_rolls_back_document_and_links(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
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"))
|
||||
document = await create_document_with_people(
|
||||
document=Document(name="Original name"),
|
||||
links=[DocumentPersonInput(person_id=person.id, role_id=role.id)],
|
||||
documents=documents,
|
||||
people=people,
|
||||
)
|
||||
assert [item.name for item in await documents.list_documents()] == ["Original name"]
|
||||
|
||||
candidate = Document(
|
||||
id=document.id,
|
||||
name="Changed name",
|
||||
created_at=document.created_at,
|
||||
updated_at=document.updated_at,
|
||||
)
|
||||
with pytest.raises(PeopleError):
|
||||
await update_document_with_people(
|
||||
document=candidate,
|
||||
links=[DocumentPersonInput(person_id=person.id, role_id=inactive.id)],
|
||||
documents=documents,
|
||||
people=people,
|
||||
)
|
||||
|
||||
persisted_documents = await documents.list_documents()
|
||||
links = await people.list_document_people(document_id=document.id)
|
||||
assert [item.name for item in persisted_documents] == ["Original name"]
|
||||
assert len(links) == 1
|
||||
assert links[0].role_id == role.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_direct_link_writes_reject_new_inactive_role_assignments(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
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"))
|
||||
document = await documents.create_document(Document(name="Role rules"))
|
||||
link = await people.add_document_person_link(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role_id=active.id,
|
||||
)
|
||||
|
||||
with pytest.raises(PeopleError, match="Inactive Person Role"):
|
||||
await people.set_document_person_role(
|
||||
document_person_id=link.id,
|
||||
role_id=inactive.id,
|
||||
)
|
||||
|
||||
unchanged = await people.set_document_person_role(
|
||||
document_person_id=link.id,
|
||||
role_id=active.id,
|
||||
)
|
||||
assert unchanged.role_id == active.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_print_projection_uses_semantic_author_and_current_text(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
people = PeopleService(session_factory=default_session_factory)
|
||||
sources = SourceService(session_factory=default_session_factory)
|
||||
jobs = JobService(session_factory=default_session_factory)
|
||||
document = await documents.create_document(Document(name="Print Me", notes="Archive note"))
|
||||
person = await people.create_person(Person(full_name="Historic Author"))
|
||||
|
||||
async with people._session_scope() as session:
|
||||
author = PersonRole(
|
||||
semantic_key="author",
|
||||
label="Creator",
|
||||
normalized_label="creator",
|
||||
)
|
||||
session.add(author)
|
||||
await session.flush()
|
||||
session.add(DocumentPerson(document_id=document.id, person_id=person.id, role_id=author.id))
|
||||
await session.commit()
|
||||
|
||||
await sources.create_source(
|
||||
Source(
|
||||
document_id=document.id,
|
||||
page_number=2,
|
||||
upload_name="page-2.png",
|
||||
filename="page-2.png",
|
||||
file_path="managed/page-2.png",
|
||||
file_hash="2" * 64,
|
||||
file_size_bytes=2,
|
||||
raw_transcription="raw second",
|
||||
revised_text="revised second",
|
||||
)
|
||||
)
|
||||
await sources.create_source(
|
||||
Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="page-1.png",
|
||||
filename="page-1.png",
|
||||
file_path="managed/page-1.png",
|
||||
file_hash="1" * 64,
|
||||
file_size_bytes=1,
|
||||
raw_transcription="raw first",
|
||||
)
|
||||
)
|
||||
await jobs.create_job(
|
||||
Job(
|
||||
document_id=document.id,
|
||||
status=JobStatus.COMPLETED,
|
||||
provider="openrouter",
|
||||
model="model-a",
|
||||
prompt_name="transcribe_document.md",
|
||||
date_created=datetime(2026, 1, 1, tzinfo=UTC),
|
||||
)
|
||||
)
|
||||
|
||||
projection = await documents.read_document_print_projection(document.id)
|
||||
|
||||
assert projection.authors == ("Historic Author",)
|
||||
assert [source.page_number for source in projection.sources] == [1, 2]
|
||||
assert [source.current_text for source in projection.sources] == ["raw first", "revised second"]
|
||||
assert [source.media_type for source in projection.sources] == ["image/png", "image/png"]
|
||||
assert projection.jobs[0].status == "completed"
|
||||
Reference in New Issue
Block a user