generated from john/python-template
Revamped the Documents, People, & Jobs too.
This commit is contained in:
+117
-307
@@ -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
|
||||
Reference in New Issue
Block a user