generated from john/python-template
This commit is contained in:
@@ -28,6 +28,7 @@ Documents manages the archival record for each historical artifact independently
|
|||||||
- # Sources reflects the count of linked Source rows for each Document.
|
- # Sources reflects the count of linked Source rows for each Document.
|
||||||
- Date display prefers exact date, then approximate date, then `Unknown`.
|
- Date display prefers exact date, then approximate date, then `Unknown`.
|
||||||
- Selecting a row opens Document Detail.
|
- Selecting a row opens Document Detail.
|
||||||
|
- Row navigation includes list context so Document Detail provides **Back to Documents**.
|
||||||
- No records displays `No documents found in repository.`
|
- No records displays `No documents found in repository.`
|
||||||
|
|
||||||
## Create and Edit Behavior
|
## Create and Edit Behavior
|
||||||
@@ -68,6 +69,7 @@ Rules:
|
|||||||
## Detail Behavior
|
## Detail Behavior
|
||||||
|
|
||||||
- The heading shows name, type, and internal ID.
|
- The heading shows name, type, and internal ID.
|
||||||
|
- The header includes a contextual back action: **Back to Documents** by default, **Back to Person** when opened from Person Detail, and **Back to Job** when opened from Job Detail.
|
||||||
- The first Source, when present, appears in the dark-room viewer.
|
- The first Source, when present, appears in the dark-room viewer.
|
||||||
- Archival Metadata shows authors, Document Type, tags, Document date (`MM-DD-YYYY` for exact dates), location, and archive identifier. Notes appear in a separate archival-notes block within the same card.
|
- Archival Metadata shows authors, Document Type, tags, Document date (`MM-DD-YYYY` for exact dates), location, and archive identifier. Notes appear in a separate archival-notes block within the same card.
|
||||||
- System Logistics shows created and updated timestamps.
|
- System Logistics shows created and updated timestamps.
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ Jobs manages transcription processing runs. A Job belongs to one Document, links
|
|||||||
- Search covers Job ID, document name, and status.
|
- Search covers Job ID, document name, and status.
|
||||||
- Status is displayed as a semantic status chip.
|
- Status is displayed as a semantic status chip.
|
||||||
- Selecting a row opens Job Detail.
|
- Selecting a row opens Job Detail.
|
||||||
|
- Global Job-list row navigation includes list context so Job Detail provides **Back to Jobs**.
|
||||||
- No records displays `No job records found in repository.`
|
- No records displays `No job records found in repository.`
|
||||||
|
|
||||||
## Create Behavior
|
## Create Behavior
|
||||||
@@ -43,8 +44,9 @@ Jobs manages transcription processing runs. A Job belongs to one Document, links
|
|||||||
## Detail and Lifecycle Behavior
|
## Detail and Lifecycle Behavior
|
||||||
|
|
||||||
- The heading shows Job ID and a status badge.
|
- The heading shows Job ID and a status badge.
|
||||||
|
- Job Detail includes a contextual back action: **Back to Jobs** by default and **Back to Document** when opened from a Document-filtered Job list.
|
||||||
- Execution Logistics shows provider, model, prompt, retry count, and last update.
|
- Execution Logistics shows provider, model, prompt, retry count, and last update.
|
||||||
- Document Links show a clickable Document Name, Sources count, and a single **View Sources** action using document filtering.
|
- Document Links show a clickable Document Name (with Job context), Sources count, and a single **View Sources** action using job filtering.
|
||||||
- Queued and processing Jobs show an auto-refresh notice and reload every four seconds.
|
- Queued and processing Jobs show an auto-refresh notice and reload every four seconds.
|
||||||
- Polling stops when the Job becomes terminal or a refresh fails.
|
- Polling stops when the Job becomes terminal or a refresh fails.
|
||||||
- Queued and processing Jobs expose **Cancel**.
|
- Queued and processing Jobs expose **Cancel**.
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ People manages reusable historical-person records. A Person may appear in many D
|
|||||||
- # Documents reflects how many linked Documents each Person is connected to.
|
- # Documents reflects how many linked Documents each Person is connected to.
|
||||||
- Birth and death values independently prefer exact date, then approximate date, then `Unknown`.
|
- Birth and death values independently prefer exact date, then approximate date, then `Unknown`.
|
||||||
- Selecting a row opens Person Detail.
|
- Selecting a row opens Person Detail.
|
||||||
|
- Row navigation includes list context so Person Detail provides **Back to People**.
|
||||||
- No records displays `No person records found in repository.`
|
- No records displays `No person records found in repository.`
|
||||||
|
|
||||||
## Create and Edit Behavior
|
## Create and Edit Behavior
|
||||||
@@ -55,6 +56,7 @@ Rules:
|
|||||||
## Detail Behavior
|
## Detail Behavior
|
||||||
|
|
||||||
- The header provides **New Document**, **Edit Person**, **Edit Photo(s)**, and **Delete**.
|
- The header provides **New Document**, **Edit Person**, **Edit Photo(s)**, and **Delete**.
|
||||||
|
- The header includes a contextual back action: **Back to People** by default, and **Back to Document** when opened from Document Detail.
|
||||||
- **New Document** opens Document creation with this Person requested for author preselection.
|
- **New Document** opens Document creation with this Person requested for author preselection.
|
||||||
- Person Detail shows a single-photo viewer with **Previous/Next** navigation; the page-level **Edit Photo(s)** header action opens photo management.
|
- Person Detail shows a single-photo viewer with **Previous/Next** navigation; the page-level **Edit Photo(s)** header action opens photo management.
|
||||||
- Photo management (upload, description edit, set-primary, delete) is intentionally moved to `/people/{person_id}/photos`.
|
- Photo management (upload, description edit, set-primary, delete) is intentionally moved to `/people/{person_id}/photos`.
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ def render_documents_table(rows: Sequence[DocumentTableRow]) -> None:
|
|||||||
],
|
],
|
||||||
default_sort_by="name",
|
default_sort_by="name",
|
||||||
search_placeholder="Search documents by title, type, or author...",
|
search_placeholder="Search documents by title, type, or author...",
|
||||||
on_row_click_id=lambda doc_id: ui.navigate.to(f"/documents/{doc_id}"),
|
on_row_click_id=lambda doc_id: ui.navigate.to(f"/documents/{doc_id}?from=documents"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Render document type using a subtle Quasar badge
|
# Render document type using a subtle Quasar badge
|
||||||
|
|||||||
@@ -57,13 +57,18 @@ def _serialize_rows(rows: Sequence[JobTableRow]) -> list[dict[str, Any]]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def render_jobs_table(rows: Sequence[JobTableRow]) -> None:
|
def render_jobs_table(rows: Sequence[JobTableRow], *, document_context_id: str | None = None) -> None:
|
||||||
"""Render jobs table with search filtering and custom status chips."""
|
"""Render jobs table with search filtering and custom status chips."""
|
||||||
if not rows:
|
if not rows:
|
||||||
with archival_card(extra_classes="p-8 text-center"):
|
with archival_card(extra_classes="p-8 text-center"):
|
||||||
render_empty_state("No job records found in repository.")
|
render_empty_state("No job records found in repository.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def detail_target(job_id: str) -> str:
|
||||||
|
if document_context_id is not None:
|
||||||
|
return f"/jobs/{job_id}?from=document&document_id={document_context_id}"
|
||||||
|
return f"/jobs/{job_id}?from=jobs"
|
||||||
|
|
||||||
table = build_table(
|
table = build_table(
|
||||||
rows=_serialize_rows(rows),
|
rows=_serialize_rows(rows),
|
||||||
columns=[
|
columns=[
|
||||||
@@ -117,7 +122,7 @@ def render_jobs_table(rows: Sequence[JobTableRow]) -> None:
|
|||||||
default_sort_by="updated_sort",
|
default_sort_by="updated_sort",
|
||||||
default_descending=True,
|
default_descending=True,
|
||||||
search_placeholder="Search jobs by ID, document, or status...",
|
search_placeholder="Search jobs by ID, document, or status...",
|
||||||
on_row_click_id=lambda job_id: ui.navigate.to(f"/jobs/{job_id}"),
|
on_row_click_id=lambda job_id: ui.navigate.to(detail_target(job_id)),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Render job execution status using themed Quasar chips
|
# Render job execution status using themed Quasar chips
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ def render_people_table(rows: Sequence[PersonTableRow]) -> None:
|
|||||||
],
|
],
|
||||||
default_sort_by="name",
|
default_sort_by="name",
|
||||||
search_placeholder="Search people by name, tags, FamilySearch ID, or dates...",
|
search_placeholder="Search people by name, tags, FamilySearch ID, or dates...",
|
||||||
on_row_click_id=lambda person_id: ui.navigate.to(f"/people/{person_id}"),
|
on_row_click_id=lambda person_id: ui.navigate.to(f"/people/{person_id}?from=people"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Custom column template adding an archival entity icon next to person's name
|
# Custom column template adding an archival entity icon next to person's name
|
||||||
|
|||||||
@@ -209,6 +209,19 @@ def register_page() -> None: # noqa: PLR0915
|
|||||||
document_service = DocumentService(session_factory=session_factory)
|
document_service = DocumentService(session_factory=session_factory)
|
||||||
render_navigation_header(current_path="/documents")
|
render_navigation_header(current_path="/documents")
|
||||||
settings = resolve_runtime_settings(request)
|
settings = resolve_runtime_settings(request)
|
||||||
|
back_label = "Back to Documents"
|
||||||
|
back_target = "/documents"
|
||||||
|
from_context = request.query_params.get("from")
|
||||||
|
if from_context == "person":
|
||||||
|
person_id = parse_uuid(request.query_params.get("person_id"))
|
||||||
|
if person_id is not None:
|
||||||
|
back_label = "Back to Person"
|
||||||
|
back_target = f"/people/{person_id}"
|
||||||
|
elif from_context == "job":
|
||||||
|
job_id = parse_uuid(request.query_params.get("job_id"))
|
||||||
|
if job_id is not None:
|
||||||
|
back_label = "Back to Job"
|
||||||
|
back_target = f"/jobs/{job_id}"
|
||||||
|
|
||||||
parsed_doc_id = parsed_record_id(document_id, noun="Document")
|
parsed_doc_id = parsed_record_id(document_id, noun="Document")
|
||||||
if parsed_doc_id is None:
|
if parsed_doc_id is None:
|
||||||
@@ -229,6 +242,7 @@ def register_page() -> None: # noqa: PLR0915
|
|||||||
page_header(document.name, subtitle=f"Type: {type_display} | ID: {document.id}")
|
page_header(document.name, subtitle=f"Type: {type_display} | ID: {document.id}")
|
||||||
|
|
||||||
with ui.row().classes("items-center gap-2"):
|
with ui.row().classes("items-center gap-2"):
|
||||||
|
ui.button(back_label, on_click=lambda: ui.navigate.to(back_target), icon="arrow_back").props("flat")
|
||||||
ui.button(
|
ui.button(
|
||||||
"Print",
|
"Print",
|
||||||
on_click=lambda: ui.navigate.to(f"/documents/{document.id}/print"),
|
on_click=lambda: ui.navigate.to(f"/documents/{document.id}/print"),
|
||||||
@@ -584,7 +598,9 @@ def _render_related_people_card(document: Document) -> None:
|
|||||||
for person in grouped[role_label]:
|
for person in grouped[role_label]:
|
||||||
ui.button(
|
ui.button(
|
||||||
person.full_name,
|
person.full_name,
|
||||||
on_click=lambda _=None, person_id=person.id: ui.navigate.to(f"/people/{person_id}"),
|
on_click=lambda _=None, person_id=person.id: ui.navigate.to(
|
||||||
|
f"/people/{person_id}?from=document&document_id={document.id}"
|
||||||
|
),
|
||||||
icon="person",
|
icon="person",
|
||||||
).props("flat dense no-caps").classes("self-start text-xs font-semibold ui-link-primary")
|
).props("flat dense no-caps").classes("self-start text-xs font-semibold ui-link-primary")
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ def register_page() -> None: # noqa: PLR0915
|
|||||||
"ui-btn-primary"
|
"ui-btn-primary"
|
||||||
)
|
)
|
||||||
ui.button("Refresh", on_click=lambda: render_table.refresh(), icon="refresh").props("flat")
|
ui.button("Refresh", on_click=lambda: render_table.refresh(), icon="refresh").props("flat")
|
||||||
|
elif parsed_document_id is not None:
|
||||||
|
ui.button(
|
||||||
|
"Back to Document",
|
||||||
|
on_click=lambda: ui.navigate.to(f"/documents/{parsed_document_id}"),
|
||||||
|
icon="arrow_back",
|
||||||
|
).props("flat")
|
||||||
|
|
||||||
@ui.refreshable
|
@ui.refreshable
|
||||||
async def render_table() -> None:
|
async def render_table() -> None:
|
||||||
@@ -86,7 +92,7 @@ def register_page() -> None: # noqa: PLR0915
|
|||||||
)
|
)
|
||||||
for job in jobs
|
for job in jobs
|
||||||
]
|
]
|
||||||
render_jobs_table(rows)
|
render_jobs_table(rows, document_context_id=str(parsed_document_id) if parsed_document_id else None)
|
||||||
|
|
||||||
await render_table()
|
await render_table()
|
||||||
|
|
||||||
@@ -241,9 +247,17 @@ def register_page() -> None: # noqa: PLR0915
|
|||||||
ui.button("Back to Jobs", on_click=lambda: ui.navigate.to("/jobs"), icon="arrow_back").props("flat")
|
ui.button("Back to Jobs", on_click=lambda: ui.navigate.to("/jobs"), icon="arrow_back").props("flat")
|
||||||
|
|
||||||
@ui.page("/jobs/{job_id}")
|
@ui.page("/jobs/{job_id}")
|
||||||
async def job_detail_page(job_id: str, session_factory: SessionFactoryDep) -> None:
|
async def job_detail_page(job_id: str, request: Request, session_factory: SessionFactoryDep) -> None:
|
||||||
jobs_service = JobService(session_factory=session_factory)
|
jobs_service = JobService(session_factory=session_factory)
|
||||||
render_navigation_header(current_path="/jobs")
|
render_navigation_header(current_path="/jobs")
|
||||||
|
back_label = "Back to Jobs"
|
||||||
|
back_target = "/jobs"
|
||||||
|
from_context = request.query_params.get("from")
|
||||||
|
if from_context == "document":
|
||||||
|
document_id = parse_uuid(request.query_params.get("document_id"))
|
||||||
|
if document_id is not None:
|
||||||
|
back_label = "Back to Document"
|
||||||
|
back_target = f"/documents/{document_id}"
|
||||||
|
|
||||||
parsed_job_id = parsed_record_id(job_id, noun="Job")
|
parsed_job_id = parsed_record_id(job_id, noun="Job")
|
||||||
if parsed_job_id is None:
|
if parsed_job_id is None:
|
||||||
@@ -262,7 +276,7 @@ def register_page() -> None: # noqa: PLR0915
|
|||||||
@ui.refreshable
|
@ui.refreshable
|
||||||
def render_detail() -> None:
|
def render_detail() -> None:
|
||||||
active_job = current_job[0]
|
active_job = current_job[0]
|
||||||
_render_job_detail_header(active_job)
|
_render_job_detail_header(active_job, back_label=back_label, back_target=back_target)
|
||||||
with ui.grid().classes("w-full grid-cols-1 md:grid-cols-2 gap-4"):
|
with ui.grid().classes("w-full grid-cols-1 md:grid-cols-2 gap-4"):
|
||||||
_render_job_logistics(active_job)
|
_render_job_logistics(active_job)
|
||||||
_render_job_document_links(active_job)
|
_render_job_document_links(active_job)
|
||||||
@@ -540,10 +554,11 @@ def _render_upload_section(uploaded_files: list[tuple[str, bytes]]) -> None:
|
|||||||
render_upload_list()
|
render_upload_list()
|
||||||
|
|
||||||
|
|
||||||
def _render_job_detail_header(job: Job) -> None:
|
def _render_job_detail_header(job: Job, *, back_label: str, back_target: str) -> None:
|
||||||
with section_header_row(classes="justify-between items-center"):
|
with section_header_row(classes="justify-between items-center"):
|
||||||
page_header(f"Job Record: {job.id}")
|
page_header(f"Job Record: {job.id}")
|
||||||
with ui.row().classes("items-center gap-2"):
|
with ui.row().classes("items-center gap-2"):
|
||||||
|
ui.button(back_label, on_click=lambda: ui.navigate.to(back_target), icon="arrow_back").props("flat")
|
||||||
archival_badge(job.status.value.upper())
|
archival_badge(job.status.value.upper())
|
||||||
|
|
||||||
if job.status in {JobStatus.QUEUED, JobStatus.PROCESSING}:
|
if job.status in {JobStatus.QUEUED, JobStatus.PROCESSING}:
|
||||||
@@ -584,7 +599,7 @@ def _render_job_document_links(job: Job) -> None:
|
|||||||
ui.label("Document Name:").classes("text-xs font-semibold ui-text-primary")
|
ui.label("Document Name:").classes("text-xs font-semibold ui-text-primary")
|
||||||
ui.button(
|
ui.button(
|
||||||
document_name,
|
document_name,
|
||||||
on_click=lambda: ui.navigate.to(f"/documents/{job.document_id}"),
|
on_click=lambda: ui.navigate.to(f"/documents/{job.document_id}?from=job&job_id={job.id}"),
|
||||||
icon="description",
|
icon="description",
|
||||||
).props("flat dense no-caps").classes("ui-link-primary text-xs")
|
).props("flat dense no-caps").classes("ui-link-primary text-xs")
|
||||||
|
|
||||||
@@ -592,7 +607,7 @@ def _render_job_document_links(job: Job) -> None:
|
|||||||
with ui.row().classes("w-full gap-2 mt-2"):
|
with ui.row().classes("w-full gap-2 mt-2"):
|
||||||
ui.button(
|
ui.button(
|
||||||
"View Sources",
|
"View Sources",
|
||||||
on_click=lambda: ui.navigate.to(f"/sources?document_id={job.document_id}"),
|
on_click=lambda: ui.navigate.to(f"/sources?job_id={job.id}"),
|
||||||
icon="description",
|
icon="description",
|
||||||
).props("flat dense text-xs").classes("ui-link-primary")
|
).props("flat dense text-xs").classes("ui-link-primary")
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from transcription.ui.components.formatters import compact_date
|
|||||||
from transcription.ui.components.formatters import family_search_url
|
from transcription.ui.components.formatters import family_search_url
|
||||||
from transcription.ui.components.formatters import google_maps_search_url
|
from transcription.ui.components.formatters import google_maps_search_url
|
||||||
from transcription.ui.components.formatters import parse_iso_date
|
from transcription.ui.components.formatters import parse_iso_date
|
||||||
|
from transcription.ui.components.formatters import parse_uuid
|
||||||
from transcription.ui.components.guards import parsed_record_id
|
from transcription.ui.components.guards import parsed_record_id
|
||||||
from transcription.ui.components.guards import render_record_not_found
|
from transcription.ui.components.guards import render_record_not_found
|
||||||
from transcription.ui.components.media_urls import resolve_media_url
|
from transcription.ui.components.media_urls import resolve_media_url
|
||||||
@@ -178,6 +179,14 @@ def register_page() -> None: # noqa: PLR0915
|
|||||||
people_service = PeopleService(session_factory=session_factory)
|
people_service = PeopleService(session_factory=session_factory)
|
||||||
photos_service = PhotosService(session_factory=session_factory)
|
photos_service = PhotosService(session_factory=session_factory)
|
||||||
render_navigation_header(current_path="/people")
|
render_navigation_header(current_path="/people")
|
||||||
|
back_label = "Back to People"
|
||||||
|
back_target = "/people"
|
||||||
|
from_context = request.query_params.get("from")
|
||||||
|
if from_context == "document":
|
||||||
|
document_id = parse_uuid(request.query_params.get("document_id"))
|
||||||
|
if document_id is not None:
|
||||||
|
back_label = "Back to Document"
|
||||||
|
back_target = f"/documents/{document_id}"
|
||||||
|
|
||||||
parsed_person_id = parsed_record_id(person_id, noun="Person")
|
parsed_person_id = parsed_record_id(person_id, noun="Person")
|
||||||
if parsed_person_id is None:
|
if parsed_person_id is None:
|
||||||
@@ -197,6 +206,7 @@ def register_page() -> None: # noqa: PLR0915
|
|||||||
page_header(person.full_name, subtitle=f"Person ID: {person.id}")
|
page_header(person.full_name, subtitle=f"Person ID: {person.id}")
|
||||||
|
|
||||||
with ui.row().classes("items-center gap-2"):
|
with ui.row().classes("items-center gap-2"):
|
||||||
|
ui.button(back_label, on_click=lambda: ui.navigate.to(back_target), icon="arrow_back").props("flat")
|
||||||
ui.button(
|
ui.button(
|
||||||
"New Document",
|
"New Document",
|
||||||
on_click=lambda: ui.navigate.to(f"/documents/new?person_id={person.id}"),
|
on_click=lambda: ui.navigate.to(f"/documents/new?person_id={person.id}"),
|
||||||
@@ -812,7 +822,7 @@ def _render_linked_documents(person: Person) -> None:
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
default_sort_by="document_name",
|
default_sort_by="document_name",
|
||||||
on_row_click_id=lambda doc_id: ui.navigate.to(f"/documents/{doc_id}"),
|
on_row_click_id=lambda doc_id: ui.navigate.to(f"/documents/{doc_id}?from=person&person_id={person.id}"),
|
||||||
show_search=False,
|
show_search=False,
|
||||||
)
|
)
|
||||||
table.add_slot(
|
table.add_slot(
|
||||||
|
|||||||
@@ -153,6 +153,36 @@ class TestDocumentsPageRendering:
|
|||||||
assert "1924-07-04" not in response.text
|
assert "1924-07-04" not in response.text
|
||||||
assert "PIPELINE JOBS" in response.text.upper()
|
assert "PIPELINE JOBS" in response.text.upper()
|
||||||
assert "Edit Document" in response.text
|
assert "Edit Document" in response.text
|
||||||
|
assert "Back to Documents" in response.text
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_document_detail_page_renders_person_context_back_button(self, app_client, seed_person_and_document):
|
||||||
|
_, client = app_client
|
||||||
|
doc_id, person_id = seed_person_and_document
|
||||||
|
|
||||||
|
response = client.get(f"/ui/documents/{doc_id}?from=person&person_id={person_id}")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Back to Person" in response.text
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_document_detail_page_renders_job_context_back_button(self, app_client):
|
||||||
|
_, client = app_client
|
||||||
|
|
||||||
|
async with session_scope() as session:
|
||||||
|
doc = Document(name="Job-linked Document")
|
||||||
|
session.add(doc)
|
||||||
|
await session.flush()
|
||||||
|
job = Job(document_id=doc.id)
|
||||||
|
session.add(job)
|
||||||
|
await session.commit()
|
||||||
|
doc_id = str(doc.id)
|
||||||
|
job_id = str(job.id)
|
||||||
|
|
||||||
|
response = client.get(f"/ui/documents/{doc_id}?from=job&job_id={job_id}")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Back to Job" in response.text
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_document_jobs_page_redirects_to_filtered_jobs(self, app_client):
|
async def test_document_jobs_page_redirects_to_filtered_jobs(self, app_client):
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ class TestJobsPageRendering:
|
|||||||
assert "Jobs for Document" in response.text
|
assert "Jobs for Document" in response.text
|
||||||
assert "Create job" not in response.text
|
assert "Create job" not in response.text
|
||||||
assert "Refresh" not in response.text
|
assert "Refresh" not in response.text
|
||||||
|
assert "Back to Document" in response.text
|
||||||
assert first_job_id in response.text
|
assert first_job_id in response.text
|
||||||
assert second_job_id not in response.text
|
assert second_job_id not in response.text
|
||||||
|
|
||||||
@@ -131,10 +132,25 @@ class TestJobsPageRendering:
|
|||||||
assert "Document Name:" in response.text
|
assert "Document Name:" in response.text
|
||||||
assert "Sources:" in response.text
|
assert "Sources:" in response.text
|
||||||
assert "View Sources" in response.text
|
assert "View Sources" in response.text
|
||||||
|
assert "Back to Jobs" in response.text
|
||||||
assert "View Linked Document" not in response.text
|
assert "View Linked Document" not in response.text
|
||||||
assert "View Linked Sources" not in response.text
|
assert "View Linked Sources" not in response.text
|
||||||
assert "updates automatically while the job is active" in response.text
|
assert "updates automatically while the job is active" in response.text
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_job_detail_page_renders_document_context_back_button(
|
||||||
|
self,
|
||||||
|
app_client,
|
||||||
|
seed_document_with_unlinked_job,
|
||||||
|
):
|
||||||
|
_, client = app_client
|
||||||
|
document_id, job_id = seed_document_with_unlinked_job
|
||||||
|
|
||||||
|
response = client.get(f"/ui/jobs/{job_id}?from=document&document_id={document_id}")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Back to Document" in response.text
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_job_cancel_page_renders_confirmation(self, app_client, seed_document_with_unlinked_job):
|
async def test_job_cancel_page_renders_confirmation(self, app_client, seed_document_with_unlinked_job):
|
||||||
_, client = app_client
|
_, client = app_client
|
||||||
|
|||||||
@@ -134,9 +134,27 @@ class TestPeoplePageRendering:
|
|||||||
assert "Open in FamilySearch" not in response.text
|
assert "Open in FamilySearch" not in response.text
|
||||||
assert "FamilySearch ID:" in response.text
|
assert "FamilySearch ID:" in response.text
|
||||||
assert "familysearch.org/tree/person/details/G8T4-MDQ" in response.text
|
assert "familysearch.org/tree/person/details/G8T4-MDQ" in response.text
|
||||||
|
assert "Back to People" in response.text
|
||||||
assert "New Document" in response.text
|
assert "New Document" in response.text
|
||||||
assert "No linked documents yet." in response.text
|
assert "No linked documents yet." in response.text
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_person_detail_page_renders_document_context_back_button(self, app_client):
|
||||||
|
_, client = app_client
|
||||||
|
|
||||||
|
async with session_scope() as session:
|
||||||
|
person = Person(given_names="Context", last_name="Person")
|
||||||
|
document = Document(name="Context Document")
|
||||||
|
session.add_all([person, document])
|
||||||
|
await session.commit()
|
||||||
|
person_id = str(person.id)
|
||||||
|
document_id = str(document.id)
|
||||||
|
|
||||||
|
response = client.get(f"/ui/people/{person_id}?from=document&document_id={document_id}")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Back to Document" in response.text
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_person_detail_page_resolves_relative_photo_path(self, app_client):
|
async def test_person_detail_page_resolves_relative_photo_path(self, app_client):
|
||||||
app, client = app_client
|
app, client = app_client
|
||||||
|
|||||||
Reference in New Issue
Block a user