Revamped the Documents, People, & Jobs too.

This commit is contained in:
Jim Lancaster
2026-08-05 13:05:41 -05:00
parent 72bc96ab3a
commit fd3ca60008
15 changed files with 1719 additions and 1338 deletions
+117 -307
View File
@@ -1,366 +1,176 @@
"""Tests for the documents page routes."""
import asyncio
from datetime import UTC
from datetime import date
from datetime import datetime
from uuid import uuid4
"""Tests for the documents page routes and action handlers."""
import pytest
from sqlmodel import select
from transcription.db import session_scope
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
from transcription.db.models import Document, DocumentPerson, DocumentPersonRole, Job, Person, Source
# --- Helper Fixtures ---
@pytest.fixture
async def seed_person_and_document():
"""Seed a Person and Document linked by DocumentPerson role."""
async with session_scope() as session:
person = Person(full_name="Zenna Cochran")
session.add(person)
await session.flush()
doc = Document(
name="Letter from Hig",
document_type="letter",
archive_identifier="ZC-1924-001",
)
session.add(doc)
await session.flush()
link = DocumentPerson(
document_id=doc.id,
person_id=person.id,
role=DocumentPersonRole.AUTHOR,
)
session.add(link)
await session.commit()
return str(doc.id), str(person.id)
# --- Integration Tests for Documents Route Handlers ---
@pytest.mark.integration
class TestDocumentsPageRendering:
"""Verify document list/detail routes render expected read states."""
"""Verify document list, detail, edit, and deletion route behaviors."""
def test_documents_page_renders_empty_state(self, app_client):
"""GET /ui/documents renders empty-state text when no records exist."""
_, client = app_client
response = client.get("/ui/documents")
assert response.status_code == 200
assert "Documents" in response.text
assert "Create new document" in response.text
assert "Archival Documents" in response.text
assert "No documents in repository yet." in response.text
def test_document_create_page_renders_fields(self, app_client):
"""GET /ui/documents/new renders document-create form fields."""
@pytest.mark.asyncio
async def test_documents_page_lists_seeded_documents(self, app_client):
_, client = app_client
async with session_scope() as session:
doc = Document(name="1924 Postcard", document_type="postcard", archive_identifier="PC-001")
session.add(doc)
await session.commit()
response = client.get("/ui/documents")
assert response.status_code == 200
assert "1924 Postcard" in response.text
assert "postcard" in response.text
assert "PC-001" in response.text
def test_document_create_page_renders_form(self, app_client):
_, client = app_client
response = client.get("/ui/documents/new")
assert response.status_code == 200
assert "Create Document" in response.text
assert "Document name is required." in response.text
assert "Document name" in response.text
assert "Document type" in response.text
assert "Author (Person)" in response.text
assert "Exact date (YYYY-MM-DD)" in response.text
assert "Approximate date" in response.text
assert "Document location" in response.text
assert "Archive identifier" in response.text
assert "Notes" in response.text
assert "Create new item" in response.text
assert "Create new person" in response.text
assert "Save document" in response.text
def test_documents_page_lists_seeded_documents(self, app_client):
"""GET /ui/documents lists seeded document cards."""
@pytest.mark.asyncio
async def test_document_detail_page_renders_bento_grid_and_metadata(
self, app_client, seed_person_and_document
):
_, client = app_client
doc_id, _ = seed_person_and_document
async def _seed_document() -> None:
async with session_scope() as session:
session.add(Document(name="Seeded Document", document_type="letter"))
await session.commit()
asyncio.run(_seed_document())
response = client.get("/ui/documents")
response = client.get(f"/ui/documents/{doc_id}")
assert response.status_code == 200
assert "Seeded Document" in response.text
assert "letter" in response.text
def test_document_detail_page_renders_metadata_and_empty_related_sections(self, app_client):
"""GET /ui/documents/{document_id} shows metadata and related empty states."""
_, client = app_client
async def _seed_document() -> str:
async with session_scope() as session:
document = Document(
name="Zenna Letter",
document_type="letter",
document_date=date(1885, 7, 13),
document_date_raw="c. 1885",
location_created="Ohio",
notes="Family archive",
archive_identifier="BOX-1-FOLDER-2",
)
session.add(document)
await session.commit()
await session.refresh(document)
return str(document.id)
document_id = asyncio.run(_seed_document())
response = client.get(f"/ui/documents/{document_id}")
assert response.status_code == 200
assert "Zenna Letter" in response.text
assert "Type: letter" in response.text
assert "Author:" in response.text
assert "Not set" in response.text
assert "Exact Date:" in response.text
assert "1885-07-13" in response.text
assert "Approx. Date:" in response.text
assert "c. 1885" in response.text
assert "Location Created:" in response.text
assert "Ohio" in response.text
assert "Archive Identifier:" in response.text
assert "BOX-1-FOLDER-2" in response.text
assert "Archival Notes:" in response.text
assert "Family archive" in response.text
assert "Created:" in response.text
assert "Updated:" in response.text
assert "No linked people yet." in response.text
assert "0 Source(s) Linked" in response.text
assert "0 Active Jobs" in response.text
assert "+ Add Source" in response.text
assert "+ Add Job" in response.text
assert "Sources" in response.text
assert "Jobs" in response.text
assert "Letter from Hig" in response.text
assert "ZC-1924-001" in response.text
assert "Zenna Cochran" in response.text
assert "Archival Metadata" in response.text
assert "Edit Document" in response.text
assert "Delete" in response.text
def test_document_detail_page_renders_related_people_sources_and_jobs(self, app_client):
"""GET /ui/documents/{document_id} shows related records when present."""
@pytest.mark.asyncio
async def test_document_jobs_page_renders_job_links(self, app_client):
_, client = app_client
async def _seed_related() -> str:
async with session_scope() as session:
document = Document(name="Roster", document_type="record")
person = Person(full_name="Jane Doe")
session.add(document)
session.add(person)
await session.flush()
async with session_scope() as session:
doc = Document(name="Doc With Job", document_type="letter")
session.add(doc)
await session.flush()
session.add(
DocumentPerson(
document_id=document.id,
person_id=person.id,
role=DocumentPersonRole.AUTHOR,
)
)
session.add(
Source(
document_id=document.id,
page_number=1,
upload_name="001_page.png",
filename="stored_001_page.png",
file_path="/tmp/stored_001_page.png",
)
)
session.add(
Job(
document_id=document.id,
)
)
await session.commit()
await session.refresh(document)
return str(document.id)
job = Job(document_id=doc.id)
session.add(job)
await session.commit()
doc_id = str(doc.id)
job_id = str(job.id)
document_id = asyncio.run(_seed_related())
response = client.get(f"/ui/documents/{document_id}")
response = client.get(f"/ui/documents/{doc_id}/jobs")
assert response.status_code == 200
assert "Jane Doe" in response.text
assert "author" in response.text
assert "Author:" in response.text
assert "1 Source(s) Linked" in response.text
assert "1 Active Jobs" in response.text
assert "Jobs for Doc With Job" in response.text
assert f"Job ID: {job_id}" in response.text
def test_document_jobs_page_filters_to_document_context(self, app_client):
@pytest.mark.asyncio
async def test_document_edit_page_prefills_existing_values(
self, app_client, seed_person_and_document
):
_, client = app_client
doc_id, _ = seed_person_and_document
async def _seed() -> str:
async with session_scope() as session:
target = Document(name="Target", document_type="letter")
other = Document(name="Other", document_type="record")
session.add(target)
session.add(other)
await session.flush()
session.add(Job(document_id=target.id))
session.add(Job(document_id=other.id))
await session.commit()
await session.refresh(target)
return str(target.id)
document_id = asyncio.run(_seed())
response = client.get(f"/ui/documents/{document_id}/jobs")
assert response.status_code == 200
assert "Jobs for Target" in response.text
assert "Jobs for Other" not in response.text
def test_document_sources_page_filters_to_document_context(self, app_client):
_, client = app_client
async def _seed() -> str:
async with session_scope() as session:
target = Document(name="Target", document_type="letter")
other = Document(name="Other", document_type="record")
session.add(target)
session.add(other)
await session.flush()
session.add(
Source(
document_id=target.id,
page_number=1,
upload_name="target_page.png",
filename="target_stored.png",
file_path="/tmp/target_stored.png",
)
)
session.add(
Source(
document_id=other.id,
page_number=1,
upload_name="other_page.png",
filename="other_stored.png",
file_path="/tmp/other_stored.png",
)
)
await session.commit()
await session.refresh(target)
return str(target.id)
document_id = asyncio.run(_seed())
response = client.get(f"/ui/sources?document_id={document_id}")
assert response.status_code == 200
assert "Sources: Target" in response.text
assert "Back to Document" in response.text
assert "target_page.png" in response.text
assert "other_page.png" not in response.text
def test_document_detail_page_rejects_invalid_id(self, app_client):
"""GET /ui/documents/{document_id} shows validation feedback for malformed IDs."""
_, client = app_client
response = client.get("/ui/documents/not-a-uuid")
assert response.status_code == 200
assert "Invalid document id" in response.text
def test_document_detail_page_handles_missing_document(self, app_client):
"""GET /ui/documents/{document_id} shows not-found state for unknown IDs."""
_, client = app_client
response = client.get(f"/ui/documents/{uuid4()}")
assert response.status_code == 200
assert "Document not found" in response.text
def test_document_edit_page_renders_expected_fields(self, app_client):
"""GET /ui/documents/{document_id}/edit renders editable fields and save controls."""
_, client = app_client
async def _seed_document() -> str:
async with session_scope() as session:
document = Document(
name="Editable Document",
document_type="memo",
document_date_raw="c. 1900",
)
session.add(document)
await session.commit()
await session.refresh(document)
return str(document.id)
document_id = asyncio.run(_seed_document())
response = client.get(f"/ui/documents/{document_id}/edit")
response = client.get(f"/ui/documents/{doc_id}/edit")
assert response.status_code == 200
assert "Edit Document Record" in response.text
assert "Document name and document type are required." in response.text
assert "Document name" in response.text
assert "Document type" in response.text
assert "Author (Person)" in response.text
assert "Exact date (YYYY-MM-DD)" in response.text
assert "Approximate date" in response.text
assert "Document location" in response.text
assert "Archive identifier" in response.text
assert "Notes" in response.text
assert "Create new item" in response.text
assert "Create new person" in response.text
assert "Save changes" in response.text
assert "Letter from Hig" in response.text
assert "ZC-1924-001" in response.text
def test_document_delete_page_shows_confirmation_when_unlinked(self, app_client):
"""GET /ui/documents/{document_id}/delete renders permanent-action confirmation if unlinked."""
@pytest.mark.asyncio
async def test_document_delete_page_blocks_deletion_when_dependencies_exist(
self, app_client
):
_, client = app_client
async def _seed_document() -> str:
async with session_scope() as session:
document = Document(name="Safe Delete", document_type="letter")
session.add(document)
await session.commit()
await session.refresh(document)
return str(document.id)
async with session_scope() as session:
doc = Document(name="Doc With Source", document_type="letter")
session.add(doc)
await session.flush()
document_id = asyncio.run(_seed_document())
source = Source(
document_id=doc.id,
page_number=1,
upload_name="page_1.png",
filename="page_1.png",
file_path="/tmp/page_1.png",
)
session.add(source)
await session.commit()
doc_id = str(doc.id)
response = client.get(f"/ui/documents/{document_id}/delete")
response = client.get(f"/ui/documents/{doc_id}/delete")
assert response.status_code == 200
assert "Delete Document" in response.text
assert "This action permanently deletes the document." in response.text
assert "Delete document permanently" in response.text
def test_document_delete_page_shows_blocked_state_when_dependencies_exist(self, app_client):
"""GET /ui/documents/{document_id}/delete explains blocked deletion with dependency categories."""
_, client = app_client
async def _seed_related() -> str:
async with session_scope() as session:
document = Document(name="Blocked Delete", document_type="record")
session.add(document)
await session.flush()
session.add(
Source(
document_id=document.id,
page_number=1,
upload_name="001_page.png",
filename="stored_001_page.png",
file_path="/tmp/stored_001_page.png",
)
)
session.add(Job(document_id=document.id))
await session.commit()
await session.refresh(document)
return str(document.id)
document_id = asyncio.run(_seed_related())
response = client.get(f"/ui/documents/{document_id}/delete")
assert response.status_code == 200
assert "Delete is blocked because related records exist." in response.text
assert "Dependencies present: Sources, Jobs" in response.text
assert "Go to Jobs" in response.text
assert "Dependencies present: Sources" in response.text
def test_job_create_page_preselects_document_query_param(self, app_client):
"""GET /ui/jobs/new?document_id=... includes the selected document in rendered state."""
@pytest.mark.asyncio
async def test_document_delete_page_allows_unlinked_document_deletion(self, app_client):
_, client = app_client
async def _seed_document() -> str:
async with session_scope() as session:
document = Document(
name="Preselected Document",
document_type="letter",
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
session.add(document)
await session.commit()
await session.refresh(document)
return str(document.id)
async with session_scope() as session:
doc = Document(name="Orphan Document", document_type="note")
session.add(doc)
await session.commit()
doc_id = str(doc.id)
document_id = asyncio.run(_seed_document())
response = client.get(f"/ui/jobs/new?document_id={document_id}")
response = client.get(f"/ui/documents/{doc_id}/delete")
assert response.status_code == 200
assert "Preselected Document" in response.text
assert "Delete Document" in response.text
assert "Delete document permanently" in response.text
assert "Delete is blocked" not in response.text