generated from john/python-template
V4.1 Mostly UI adjustments by GC
This commit is contained in:
@@ -10,9 +10,11 @@ from transcription.db.models import JobSource
|
||||
from transcription.db.models import JobSourceStatus
|
||||
from transcription.db.models import Person
|
||||
from transcription.db.models import Source
|
||||
from transcription.errors import ErrorCategory
|
||||
from transcription.services.documents import DocumentDeleteBlockedError
|
||||
from transcription.services.documents import DocumentService
|
||||
from transcription.services.jobs import JobService
|
||||
from transcription.services.people import PeopleError
|
||||
from transcription.services.people import PeopleService
|
||||
from transcription.services.sources import SourceDeleteBlockedError
|
||||
from transcription.services.sources import SourceService
|
||||
@@ -53,6 +55,28 @@ async def test_people_service_handles_person_and_document_person_crud(default_se
|
||||
assert len(await people_service.list_document_people(document_id=document.id)) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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 ")
|
||||
)
|
||||
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")
|
||||
)
|
||||
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")
|
||||
)
|
||||
assert malformed.value.category == ErrorCategory.VALIDATION
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transcription_service_manages_source_crud(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
@@ -88,6 +112,42 @@ async def test_transcription_service_manages_source_crud(default_session_factory
|
||||
assert len(await transcriptions.list_sources(document_id=document.id)) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_source_navigation_is_bounded_to_ordered_document(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
sources = SourceService(session_factory=default_session_factory)
|
||||
document = await documents.create_document(Document(id=uuid4(), name="ordered"))
|
||||
other = await documents.create_document(Document(id=uuid4(), name="other"))
|
||||
|
||||
first, second, third, _foreign = [
|
||||
await sources.create_source(
|
||||
Source(
|
||||
document_id=document_id,
|
||||
page_number=page_number,
|
||||
upload_name=f"page-{page_number}.jpg",
|
||||
filename=f"page-{page_number}.jpg",
|
||||
file_path=f"uploads/page-{page_number}.jpg",
|
||||
file_hash=str(page_number) * 64,
|
||||
file_size_bytes=1,
|
||||
)
|
||||
)
|
||||
for document_id, page_number in [
|
||||
(document.id, 1),
|
||||
(document.id, 2),
|
||||
(document.id, 3),
|
||||
(other.id, 2),
|
||||
]
|
||||
]
|
||||
|
||||
first_navigation = await sources.read_source_navigation(first.id)
|
||||
middle_navigation = await sources.read_source_navigation(second.id)
|
||||
last_navigation = await sources.read_source_navigation(third.id)
|
||||
|
||||
assert (first_navigation.previous_id, first_navigation.next_id) == (None, second.id)
|
||||
assert (middle_navigation.previous_id, middle_navigation.next_id) == (first.id, third.id)
|
||||
assert (last_navigation.previous_id, last_navigation.next_id) == (second.id, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transcription_service_job_source_crud_uses_caller_session(default_session_factory):
|
||||
transcriptions = SourceService(session_factory=default_session_factory)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy import text
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
@@ -79,6 +80,38 @@ async def test_create_all_seeds_default_registry_rows(tmp_path):
|
||||
await dispose_database_runtime()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_all_upgrades_existing_person_table_for_family_search(tmp_path):
|
||||
settings = Settings(
|
||||
openrouter_api_key="test-key",
|
||||
database=SqliteSettings(path=str(tmp_path / "upgrade.db")),
|
||||
environment="test",
|
||||
)
|
||||
runtime = initialize_database_runtime(settings=settings)
|
||||
|
||||
try:
|
||||
async with runtime.engine.begin() as connection:
|
||||
await connection.execute(
|
||||
text("CREATE TABLE person (id CHAR(32) PRIMARY KEY NOT NULL, full_name VARCHAR NOT NULL)")
|
||||
)
|
||||
|
||||
await create_all(engine=runtime.engine)
|
||||
async with runtime.engine.connect() as connection:
|
||||
columns, indexes = await connection.run_sync(
|
||||
lambda sync_connection: (
|
||||
{column["name"] for column in inspect(sync_connection).get_columns("person")},
|
||||
inspect(sync_connection).get_indexes("person"),
|
||||
)
|
||||
)
|
||||
|
||||
assert "family_search_id" in columns
|
||||
assert any(
|
||||
index["column_names"] == ["family_search_id"] and index["unique"] for index in indexes
|
||||
)
|
||||
finally:
|
||||
await dispose_database_runtime()
|
||||
|
||||
|
||||
def test_bootstrap_policy_production_defaults_false():
|
||||
settings = Settings(openrouter_api_key="test-key", environment="production")
|
||||
assert settings.should_bootstrap_schema is False
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
"""Tests for the V2 SQLModel persistence layer and relationships."""
|
||||
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import DocumentType
|
||||
from transcription.db.models import DocumentPerson
|
||||
from transcription.db.models import DocumentPersonRole
|
||||
from transcription.db.models import DocumentType
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import JobSource
|
||||
from transcription.db.models import JobSourceStatus
|
||||
@@ -181,6 +179,14 @@ class TestSourceModel:
|
||||
|
||||
|
||||
class TestPersonAndDocumentPersonModel:
|
||||
def test_family_search_id_is_unique_when_present(self, session):
|
||||
session.add(Person(full_name="First Person", family_search_id="G8T4-MDQ"))
|
||||
session.commit()
|
||||
|
||||
session.add(Person(full_name="Second Person", family_search_id="G8T4-MDQ"))
|
||||
with pytest.raises(IntegrityError):
|
||||
session.commit()
|
||||
|
||||
def test_document_person_role_is_unique_per_document_person(self, session):
|
||||
document = _persist_document(session)
|
||||
person = _persist_person(session)
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
"""Tests for the documents page routes and action handlers."""
|
||||
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlmodel import select
|
||||
|
||||
from transcription.db import session_scope
|
||||
from transcription.db.models import Document, DocumentPerson, DocumentPersonRole, Job, Person, Source
|
||||
|
||||
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 Source
|
||||
|
||||
# --- Helper Fixtures ---
|
||||
|
||||
@@ -68,6 +73,8 @@ class TestDocumentsPageRendering:
|
||||
assert "1924 Postcard" in response.text
|
||||
assert "postcard" in response.text
|
||||
assert "PC-001" in response.text
|
||||
assert "Document Date" in response.text
|
||||
assert "Author" in response.text
|
||||
|
||||
def test_document_create_page_renders_form(self, app_client):
|
||||
_, client = app_client
|
||||
@@ -80,6 +87,24 @@ class TestDocumentsPageRendering:
|
||||
assert "Linked People by Role" in response.text
|
||||
assert "Document type" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_create_page_preselects_person_with_disambiguating_label(self, app_client):
|
||||
_, client = app_client
|
||||
async with session_scope() as session:
|
||||
person = Person(
|
||||
full_name="Albert Edward Higgins",
|
||||
display_name="Hig",
|
||||
birth_date=date(1885, 1, 2),
|
||||
)
|
||||
session.add(person)
|
||||
await session.commit()
|
||||
person_id = str(person.id)
|
||||
|
||||
response = client.get(f"/ui/documents/new?person_id={person_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Hig - Albert Edward Higgins (1885)" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_detail_page_renders_bento_grid_and_metadata(
|
||||
self, app_client, seed_person_and_document
|
||||
@@ -93,6 +118,8 @@ class TestDocumentsPageRendering:
|
||||
assert "Letter from Hig" in response.text
|
||||
assert "ZC-1924-001" in response.text
|
||||
assert "Zenna Cochran" in response.text
|
||||
assert f"/ui/people/{seed_person_and_document[1]}" in response.text
|
||||
assert "PIPELINE JOBS" in response.text.upper()
|
||||
assert "Edit Document" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -176,4 +203,4 @@ class TestDocumentsPageRendering:
|
||||
assert response.status_code == 200
|
||||
assert "Delete Document" in response.text
|
||||
assert "Delete document permanently" in response.text
|
||||
assert "Delete is blocked" not in response.text
|
||||
assert "Delete is blocked" not in response.text
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
from datetime import date
|
||||
|
||||
from transcription.db.models import Person
|
||||
from transcription.ui.components.formatters import compact_date
|
||||
from transcription.ui.components.formatters import family_search_url
|
||||
from transcription.ui.components.formatters import person_selector_label
|
||||
|
||||
|
||||
def test_compact_date_prefers_exact_then_approximate_then_unknown():
|
||||
assert compact_date(date(1924, 3, 2), "about 1924") == "1924-03-02"
|
||||
assert compact_date(None, "about 1924") == "about 1924"
|
||||
assert compact_date(None, " ") == "Unknown"
|
||||
|
||||
|
||||
def test_person_selector_label_disambiguates_without_changing_identity():
|
||||
person = Person(
|
||||
full_name="Albert Edward Higgins",
|
||||
display_name="Hig",
|
||||
birth_date=date(1885, 1, 2),
|
||||
)
|
||||
assert person_selector_label(person) == "Hig - Albert Edward Higgins (1885)"
|
||||
|
||||
approximate = Person(
|
||||
full_name="Albert Edward Higgins",
|
||||
display_name="Hig",
|
||||
birth_date_raw="about 1912",
|
||||
)
|
||||
assert person_selector_label(approximate) == "Hig - Albert Edward Higgins (1912)"
|
||||
|
||||
|
||||
def test_family_search_url_uses_fixed_person_details_route():
|
||||
assert family_search_url("G8T4-MDQ") == (
|
||||
"https://www.familysearch.org/tree/person/details/G8T4-MDQ"
|
||||
)
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlmodel import select
|
||||
|
||||
from transcription.db import session_scope
|
||||
from transcription.db.models import Document, Job, JobSourceStatus, JobStatus
|
||||
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import JobStatus
|
||||
|
||||
# --- Helper Fixtures ---
|
||||
|
||||
@@ -99,6 +99,7 @@ class TestJobsPageRendering:
|
||||
assert "gpt-4o" in response.text
|
||||
assert "View Linked Document" in response.text
|
||||
assert "View Linked Sources" in response.text
|
||||
assert "updates automatically while the job is active" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_job_cancel_page_renders_confirmation(
|
||||
@@ -164,4 +165,4 @@ class TestJobsPageRendering:
|
||||
assert response.status_code == 200
|
||||
assert "Delete Processing Job" in response.text
|
||||
assert "Delete job permanently" in response.text
|
||||
assert "Delete is blocked" not in response.text
|
||||
assert "Delete is blocked" not in response.text
|
||||
|
||||
@@ -6,7 +6,10 @@ from uuid import uuid4
|
||||
import pytest
|
||||
|
||||
from transcription.db import session_scope
|
||||
from transcription.db.models import Document, DocumentPerson, DocumentPersonRole, Person
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import DocumentPerson
|
||||
from transcription.db.models import DocumentPersonRole
|
||||
from transcription.db.models import Person
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@@ -47,6 +50,7 @@ class TestPeoplePageRendering:
|
||||
assert "Full name is required." in response.text
|
||||
assert "Birth date (YYYY-MM-DD)" in response.text
|
||||
assert "Death date (YYYY-MM-DD)" in response.text
|
||||
assert "FamilySearch ID" in response.text
|
||||
assert "Biography" in response.text
|
||||
assert "Save person" in response.text
|
||||
|
||||
@@ -67,6 +71,7 @@ class TestPeoplePageRendering:
|
||||
death_place="Arlington",
|
||||
biography="Computer pioneer",
|
||||
portrait_path="/images/grace.jpg",
|
||||
family_search_id="G8T4-MDQ",
|
||||
)
|
||||
session.add(person)
|
||||
await session.commit()
|
||||
@@ -87,6 +92,9 @@ class TestPeoplePageRendering:
|
||||
assert "Computer pioneer" in response.text
|
||||
assert "Created:" in response.text
|
||||
assert "Updated:" in response.text
|
||||
assert "Open in FamilySearch" in response.text
|
||||
assert "familysearch.org/tree/person/details/G8T4-MDQ" in response.text
|
||||
assert "New Document" in response.text
|
||||
assert "No linked documents yet." in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -182,4 +190,4 @@ class TestPeoplePageRendering:
|
||||
assert response.status_code == 200
|
||||
assert "Delete Person Record" in response.text
|
||||
assert "This action permanently deletes the person record." in response.text
|
||||
assert "Delete person permanently" in response.text
|
||||
assert "Delete person permanently" in response.text
|
||||
|
||||
@@ -4,11 +4,13 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from transcription.db import session_scope
|
||||
from transcription.db.models import Document, Job, JobSourceStatus, JobStatus, Source
|
||||
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import JobSourceStatus
|
||||
from transcription.db.models import JobStatus
|
||||
from transcription.db.models import Source
|
||||
|
||||
# --- Unit Tests for Model @property Definitions ---
|
||||
|
||||
@@ -99,6 +101,7 @@ class TestSourcesPageRendering:
|
||||
assert response.status_code == 200
|
||||
assert "page_one.png" in response.text
|
||||
assert "Source Document" in response.text
|
||||
assert "Stored Filename" not in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sources_page_filters_to_document_context(self, app_client):
|
||||
@@ -211,6 +214,8 @@ class TestSourcesPageRendering:
|
||||
assert "original transcription text" in response.text
|
||||
assert "human revision text" in response.text
|
||||
assert "Save revision" in response.text
|
||||
assert "Previous Page" in response.text
|
||||
assert "Next Page" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_source_delete_page_blocks_when_source_is_job_linked(
|
||||
|
||||
Reference in New Issue
Block a user