generated from john/python-template
@@ -71,7 +71,7 @@ Rules:
|
||||
- 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.
|
||||
- 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 (linked to Google Maps when present), and archive identifier. Notes appear in a separate archival-notes block within the same card.
|
||||
- System Logistics shows created and updated timestamps.
|
||||
- Related People are grouped by role and link to Person Detail.
|
||||
- **Sources & Pipeline Jobs** shows counts and actions for filtered Sources, Document Jobs, and adding a Job.
|
||||
|
||||
@@ -20,7 +20,7 @@ The list accepts optional `document_id` and `job_id` query parameters. Document
|
||||
- Global context provides **Create Job**.
|
||||
- Filtered context provides **Back to Document** or **Back to Job**.
|
||||
- Rows are ordered by page number and then upload name.
|
||||
- Columns are Document Name, Page Number, Upload Title, Status, and Error Detail.
|
||||
- Columns are Upload Title, Page Number, Document Name, Status, and Error Detail.
|
||||
- Document Name, Upload Title, and Error Detail are left-aligned; Status is centered.
|
||||
- Status labels are presented in uppercase for consistency with Jobs.
|
||||
- Stored Filename is intentionally absent from the list.
|
||||
|
||||
@@ -53,9 +53,9 @@ def render_sources_table(rows: Sequence[SourceTableRow]) -> None:
|
||||
rows=_serialize_rows(rows),
|
||||
columns=[
|
||||
{
|
||||
"name": "document_name",
|
||||
"label": "Document Name",
|
||||
"field": "document_name",
|
||||
"name": "upload_name",
|
||||
"label": "Upload Title",
|
||||
"field": "upload_name",
|
||||
"sortable": True,
|
||||
"classes": "font-serif text-left ui-table-cell-wrap",
|
||||
"align": "left",
|
||||
@@ -69,9 +69,9 @@ def render_sources_table(rows: Sequence[SourceTableRow]) -> None:
|
||||
"style": "width: 10%;",
|
||||
},
|
||||
{
|
||||
"name": "upload_name",
|
||||
"label": "Upload Title",
|
||||
"field": "upload_name",
|
||||
"name": "document_name",
|
||||
"label": "Document Name",
|
||||
"field": "document_name",
|
||||
"sortable": True,
|
||||
"classes": "font-serif text-left ui-table-cell-wrap",
|
||||
"align": "left",
|
||||
|
||||
@@ -27,10 +27,12 @@ from transcription.ui.components.confirm_delete import dependency_summary
|
||||
from transcription.ui.components.confirm_delete import render_delete_actions
|
||||
from transcription.ui.components.confirm_delete import render_delete_blocked_notice
|
||||
from transcription.ui.components.data_display import archival_badge
|
||||
from transcription.ui.components.data_display import metadata_link_row
|
||||
from transcription.ui.components.data_display import metadata_row
|
||||
from transcription.ui.components.error_presenter import run_ui_action
|
||||
from transcription.ui.components.error_presenter import show_error
|
||||
from transcription.ui.components.formatters import compact_date
|
||||
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_uuid
|
||||
from transcription.ui.components.guards import parsed_record_id
|
||||
@@ -560,7 +562,14 @@ def _render_bento_metadata_zone(document: Document) -> None:
|
||||
)
|
||||
metadata_row("Tags:", ", ".join(tags) if tags else "Not set")
|
||||
metadata_row("Document Date:", _detail_document_date(document.document_date, document.document_date_raw))
|
||||
metadata_row("Location Created:", document.location_created or "Not set")
|
||||
if document.location_created:
|
||||
metadata_link_row(
|
||||
"Location Created:",
|
||||
document.location_created,
|
||||
google_maps_search_url(document.location_created),
|
||||
)
|
||||
else:
|
||||
metadata_row("Location Created:", "Not set")
|
||||
metadata_row("Archive Identifier:", document.archive_identifier or "Not set")
|
||||
|
||||
with ui.column().classes("w-full mt-2"):
|
||||
|
||||
@@ -655,13 +655,22 @@ async def _render_person_photo_zone(
|
||||
active_index = [0]
|
||||
|
||||
with ui.column().classes("col-span-12 lg:col-span-4"), archival_card(title="Photos", extra_classes="gap-3"):
|
||||
_render_photo_viewer_with_navigation(
|
||||
photos=photos,
|
||||
active_index=active_index,
|
||||
settings=settings,
|
||||
request=request,
|
||||
empty_message="No portrait photo uploaded yet.",
|
||||
)
|
||||
|
||||
@ui.refreshable
|
||||
def render_photo_viewer() -> None:
|
||||
def refresh_photo_viewer() -> None:
|
||||
render_photo_viewer.refresh()
|
||||
|
||||
_render_photo_viewer_with_navigation(
|
||||
photos=photos,
|
||||
active_index=active_index,
|
||||
settings=settings,
|
||||
request=request,
|
||||
empty_message="No portrait photo uploaded yet.",
|
||||
on_change=refresh_photo_viewer,
|
||||
)
|
||||
|
||||
render_photo_viewer()
|
||||
|
||||
|
||||
def _shift_gallery_index(*, photos: list, active_index: list[int], step: int) -> None:
|
||||
|
||||
@@ -37,6 +37,7 @@ async def seed_person_and_document():
|
||||
document_type_id=letter_type.id,
|
||||
archive_identifier="ZC-1924-001",
|
||||
document_date=date(1924, 7, 4),
|
||||
location_created="Salt Lake City, Utah",
|
||||
)
|
||||
session.add(doc)
|
||||
await session.flush()
|
||||
@@ -151,6 +152,7 @@ class TestDocumentsPageRendering:
|
||||
assert "Letter" in response.text
|
||||
assert "07-04-1924" in response.text
|
||||
assert "1924-07-04" not in response.text
|
||||
assert "google.com/maps/search/?api=1&query=Salt+Lake+City%2C+Utah" in response.text
|
||||
assert "PIPELINE JOBS" in response.text.upper()
|
||||
assert "Edit Document" in response.text
|
||||
assert "Back to Documents" in response.text
|
||||
|
||||
@@ -186,6 +186,45 @@ class TestPeoplePageRendering:
|
||||
assert response.text.count("Edit Photo(s)") == 1
|
||||
assert "Upload photo(s)" not in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_person_detail_page_shows_gallery_navigation_for_multiple_photos(self, app_client):
|
||||
app, client = app_client
|
||||
upload_dirs = {app.state.settings.upload_dir, get_settings().upload_dir}
|
||||
for upload_dir in upload_dirs:
|
||||
primary_file = upload_dir / "photos" / "portrait-primary.png"
|
||||
secondary_file = upload_dir / "photos" / "portrait-secondary.png"
|
||||
primary_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
primary_file.write_bytes(b"portrait-primary")
|
||||
secondary_file.write_bytes(b"portrait-secondary")
|
||||
|
||||
async with session_scope() as session:
|
||||
person = Person(given_names="Gallery", last_name="Navigation")
|
||||
session.add(person)
|
||||
await session.flush()
|
||||
session.add_all(
|
||||
[
|
||||
Photo(
|
||||
person_id=person.id,
|
||||
path="photos/portrait-primary.png",
|
||||
is_primary=True,
|
||||
),
|
||||
Photo(
|
||||
person_id=person.id,
|
||||
path="photos/portrait-secondary.png",
|
||||
is_primary=False,
|
||||
),
|
||||
]
|
||||
)
|
||||
await session.commit()
|
||||
person_id = str(person.id)
|
||||
|
||||
response = client.get(f"/ui/people/{person_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Previous" in response.text
|
||||
assert "Next" in response.text
|
||||
assert "1 of 2" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_person_photos_page_renders_photo_management_controls(self, app_client):
|
||||
app, client = app_client
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for the sources page routes and Source model properties."""
|
||||
|
||||
import re
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -198,6 +199,15 @@ class TestSourcesPageRendering:
|
||||
assert "page_one.png" in response.text
|
||||
assert "Source Document" in response.text
|
||||
assert "Stored Filename" not in response.text
|
||||
assert re.search(
|
||||
r'"name":"upload_name","label":"Upload Title".*'
|
||||
r'"name":"page_number","label":"Page Number".*'
|
||||
r'"name":"document_name","label":"Document Name".*'
|
||||
r'"name":"job_source_status","label":"Status".*'
|
||||
r'"name":"job_source_error_detail","label":"Error Detail"',
|
||||
response.text,
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sources_page_filters_to_document_context(self, app_client):
|
||||
|
||||
Reference in New Issue
Block a user