generated from john/python-template
@@ -50,6 +50,7 @@ Optional:
|
||||
Rules:
|
||||
|
||||
- Exact date must parse as `YYYY-MM-DD`; browser presentation may follow locale.
|
||||
- The exact-date input is labeled **Document date**.
|
||||
- Existing people appear with disambiguating labels.
|
||||
- Tag assignment supports selecting existing tags and adding new labels inline.
|
||||
- **Create new person** opens Person creation.
|
||||
@@ -68,7 +69,7 @@ Rules:
|
||||
|
||||
- The heading shows name, type, and internal ID.
|
||||
- The first Source, when present, appears in the dark-room viewer.
|
||||
- Archival Metadata shows authors, Document Type, tags, compact Document date, 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.
|
||||
- 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,8 +20,8 @@ People manages reusable historical-person records. A Person may appear in many D
|
||||
- The title is **Archival Entities: People**.
|
||||
- **Create new person** opens the create route.
|
||||
- The table defaults to Last Name order and supports search and column sorting.
|
||||
- Columns are Last Name, First & Middle, FamilySearch ID, Birth Date, Death Date, and # Documents.
|
||||
- Last Name and First & Middle are left-aligned; FamilySearch ID, date columns, and # Documents are centered.
|
||||
- Columns are Last Name, First & Middle, Tags, FamilySearch ID, Birth Date, Death Date, and # Documents.
|
||||
- Name and Tags are left-aligned; FamilySearch ID, date columns, and # Documents are centered.
|
||||
- # Documents reflects how many linked Documents each Person is connected to.
|
||||
- Birth and death values independently prefer exact date, then approximate date, then `Unknown`.
|
||||
- Selecting a row opens Person Detail.
|
||||
|
||||
@@ -22,6 +22,7 @@ The list accepts optional `document_id` and `job_id` query parameters. Document
|
||||
- Rows are ordered by page number and then upload name.
|
||||
- Columns are Document Name, Page Number, Upload Title, 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.
|
||||
- Selecting a row opens Source Detail.
|
||||
- No records displays `No source asset records found in repository.`
|
||||
@@ -39,7 +40,7 @@ The list accepts optional `document_id` and `job_id` query parameters. Document
|
||||
- Wide+narrow landscape images switch to a stacked left layout (image above Editable Revision) with metadata on the right.
|
||||
- Editable Revision is seeded from an existing revision or the preferred machine transcription.
|
||||
- Source Metadata shows upload name, stored filename, page number, Document Name, Document ID, and stored path. Source ID appears in the page-header subtitle.
|
||||
- SourceJob Metadata shows latest status, Job ID, execution time, provider, model, prompt, and failure detail.
|
||||
- SourceJob Metadata shows latest status (uppercase display), Job ID, execution time, provider, model, prompt, and failure detail.
|
||||
- Revision Logistics shows revised state, last-revised time, and upload time.
|
||||
- Candidate Machine Transcriptions appears below the image/revision area, remains compact until expanded, then compares it with the preferred
|
||||
machine result and requires confirmation before **Use this transcription**.
|
||||
|
||||
@@ -19,8 +19,8 @@ class PersonTableRow:
|
||||
"""Read model consumed by the people table component."""
|
||||
|
||||
id: UUID
|
||||
last_name: str
|
||||
given_names: str
|
||||
name: str
|
||||
tags: str
|
||||
family_search_id: str
|
||||
birth_date: str
|
||||
death_date: str
|
||||
@@ -31,8 +31,8 @@ def _serialize_rows(rows: Sequence[PersonTableRow]) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"id": str(row.id),
|
||||
"last_name": row.last_name,
|
||||
"given_names": row.given_names,
|
||||
"name": row.name,
|
||||
"tags": row.tags or "Not set",
|
||||
"family_search_id": row.family_search_id or "Not set",
|
||||
"birth_date": row.birth_date or "Unknown",
|
||||
"death_date": row.death_date or "Unknown",
|
||||
@@ -53,18 +53,18 @@ def render_people_table(rows: Sequence[PersonTableRow]) -> None:
|
||||
rows=_serialize_rows(rows),
|
||||
columns=[
|
||||
{
|
||||
"name": "last_name",
|
||||
"label": "Last Name",
|
||||
"field": "last_name",
|
||||
"name": "name",
|
||||
"label": "Last Name, First & Middle",
|
||||
"field": "name",
|
||||
"sortable": True,
|
||||
"classes": "font-serif font-semibold text-left ui-table-cell-wrap",
|
||||
},
|
||||
{
|
||||
"name": "given_names",
|
||||
"label": "First & Middle",
|
||||
"field": "given_names",
|
||||
"name": "tags",
|
||||
"label": "Tags",
|
||||
"field": "tags",
|
||||
"sortable": True,
|
||||
"classes": "font-serif font-semibold text-left ui-table-cell-wrap",
|
||||
"classes": "text-left ui-table-cell-wrap",
|
||||
},
|
||||
{"name": "family_search_id", "label": "FamilySearch ID", "field": "family_search_id", "sortable": True},
|
||||
{
|
||||
@@ -92,14 +92,14 @@ def render_people_table(rows: Sequence[PersonTableRow]) -> None:
|
||||
"classes": "font-mono",
|
||||
},
|
||||
],
|
||||
default_sort_by="last_name",
|
||||
search_placeholder="Search people by last name, given names, FamilySearch ID, or dates...",
|
||||
default_sort_by="name",
|
||||
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}"),
|
||||
)
|
||||
|
||||
# Custom column template adding an archival entity icon next to person's name
|
||||
table.add_slot(
|
||||
"body-cell-last_name",
|
||||
"body-cell-name",
|
||||
r"""
|
||||
<q-td :props="props">
|
||||
<div class="row items-center q-gutter-x-xs">
|
||||
|
||||
@@ -35,7 +35,7 @@ def _serialize_rows(rows: Sequence[SourceTableRow]) -> list[dict[str, Any]]:
|
||||
"upload_name": row.upload_name,
|
||||
"document_id": str(row.document_id),
|
||||
"document_name": row.document_name or "-",
|
||||
"job_source_status": row.job_source_status or "-",
|
||||
"job_source_status": (row.job_source_status or "-").upper(),
|
||||
"job_source_error_detail": row.job_source_error_detail or "-",
|
||||
}
|
||||
for row in rows
|
||||
@@ -109,7 +109,7 @@ def render_sources_table(rows: Sequence[SourceTableRow]) -> None:
|
||||
dense
|
||||
square
|
||||
size="sm"
|
||||
:class="`ui-status ui-status--${props.value}`"
|
||||
:class="`ui-status ui-status--${props.value.toLowerCase()}`"
|
||||
>
|
||||
{{ props.value }}
|
||||
</q-chip>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
@@ -448,7 +449,7 @@ def _render_document_form_fields(
|
||||
with ui.row().classes("w-full gap-3 grid grid-cols-1 md:grid-cols-2"):
|
||||
date_input = (
|
||||
ui.input(
|
||||
label="Exact date (YYYY-MM-DD)",
|
||||
label="Document date",
|
||||
value=document.document_date.isoformat() if document and document.document_date else "",
|
||||
)
|
||||
.props('outlined type="date"')
|
||||
@@ -542,7 +543,7 @@ def _render_bento_metadata_zone(document: Document) -> None:
|
||||
key=str.casefold,
|
||||
)
|
||||
metadata_row("Tags:", ", ".join(tags) if tags else "Not set")
|
||||
metadata_row("Document Date:", compact_date(document.document_date, document.document_date_raw))
|
||||
metadata_row("Document Date:", _detail_document_date(document.document_date, document.document_date_raw))
|
||||
metadata_row("Location Created:", document.location_created or "Not set")
|
||||
metadata_row("Archive Identifier:", document.archive_identifier or "Not set")
|
||||
|
||||
@@ -555,6 +556,12 @@ def _render_bento_metadata_zone(document: Document) -> None:
|
||||
ui.label(f"Updated: {document.updated_at.isoformat()}").classes("text-[11px] ui-text-muted")
|
||||
|
||||
|
||||
def _detail_document_date(exact: date | None, approximate: str | None) -> str:
|
||||
if exact is not None:
|
||||
return exact.strftime("%m-%d-%Y")
|
||||
return (approximate or "").strip() or "Unknown"
|
||||
|
||||
|
||||
def _render_bento_relations_zone(document: Document) -> None:
|
||||
with ui.column().classes("col-span-12 lg:col-span-4 gap-4"):
|
||||
_render_related_people_card(document)
|
||||
|
||||
@@ -313,7 +313,7 @@ def register_page() -> None: # noqa: PLR0915
|
||||
|
||||
with archival_card(extra_classes="gap-2"):
|
||||
ui.label(f"Job ID: {job.id}").classes("text-sm font-semibold font-mono ui-text-primary")
|
||||
metadata_row("Current Status:", job.status.value)
|
||||
metadata_row("Current Status:", job.status.value.upper())
|
||||
ui.label(
|
||||
"Cancel stops processing and marks remaining non-transcribed sources as cancelled. "
|
||||
"Cancelled sources can be resubmitted."
|
||||
@@ -369,7 +369,7 @@ def register_page() -> None: # noqa: PLR0915
|
||||
|
||||
with archival_card(extra_classes="gap-2"):
|
||||
ui.label(f"Job ID: {job.id}").classes("text-sm font-semibold font-mono ui-text-primary")
|
||||
metadata_row("Current Status:", job.status.value)
|
||||
metadata_row("Current Status:", job.status.value.upper())
|
||||
metadata_row("Resubmittable Sources:", str(resubmittable_count))
|
||||
ui.label(
|
||||
"Resubmit queues failed and cancelled linked sources. "
|
||||
|
||||
@@ -98,8 +98,13 @@ def register_page() -> None: # noqa: PLR0915
|
||||
rows = [
|
||||
PersonTableRow(
|
||||
id=person.id,
|
||||
last_name=person.last_name,
|
||||
given_names=person.given_names,
|
||||
name=f"{person.last_name}, {person.given_names}",
|
||||
tags=", ".join(
|
||||
sorted(
|
||||
[link.tag_ref.label for link in person.person_tags if link.tag_ref is not None],
|
||||
key=str.casefold,
|
||||
)
|
||||
),
|
||||
family_search_id=person.family_search_id or "",
|
||||
birth_date=compact_date(person.birth_date, person.birth_date_raw),
|
||||
death_date=compact_date(person.death_date, person.death_date_raw),
|
||||
|
||||
@@ -456,7 +456,7 @@ def _render_source_job_metadata_zone(
|
||||
status = latest_job_source.status.value
|
||||
with ui.row().classes("w-full justify-between items-center mb-2"):
|
||||
ui.label("Latest Status").classes("text-xs ui-text-muted")
|
||||
archival_badge(status)
|
||||
archival_badge(status.upper())
|
||||
|
||||
metadata_row("Job ID:", str(latest_job_source.job_id))
|
||||
metadata_row(
|
||||
|
||||
@@ -36,6 +36,7 @@ async def seed_person_and_document():
|
||||
name="Letter from Hig",
|
||||
document_type_id=letter_type.id,
|
||||
archive_identifier="ZC-1924-001",
|
||||
document_date=date(1924, 7, 4),
|
||||
)
|
||||
session.add(doc)
|
||||
await session.flush()
|
||||
@@ -148,6 +149,8 @@ class TestDocumentsPageRendering:
|
||||
assert "Zenna Cochran" in response.text
|
||||
assert "Document Type:" in response.text
|
||||
assert "Letter" in response.text
|
||||
assert "07-04-1924" in response.text
|
||||
assert "1924-07-04" not in response.text
|
||||
assert "PIPELINE JOBS" in response.text.upper()
|
||||
assert "Edit Document" in response.text
|
||||
|
||||
@@ -183,6 +186,8 @@ class TestDocumentsPageRendering:
|
||||
assert response.status_code == 200
|
||||
assert "Edit Document Record" in response.text
|
||||
assert "Letter from Hig" in response.text
|
||||
assert "Document date" in response.text
|
||||
assert "Exact date (YYYY-MM-DD)" not in response.text
|
||||
assert "ZC-1924-001" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -11,9 +11,11 @@ from transcription.db import session_scope
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import DocumentPerson
|
||||
from transcription.db.models import Person
|
||||
from transcription.db.models import PersonTag
|
||||
from transcription.db.models import PersonRole
|
||||
from transcription.db.models import Photo
|
||||
from transcription.db.models import Source
|
||||
from transcription.db.models import Tag
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@@ -35,16 +37,20 @@ class TestPeoplePageRendering:
|
||||
_, client = app_client
|
||||
|
||||
async with session_scope() as session:
|
||||
session.add(Person(given_names="Ada", last_name="Lovelace"))
|
||||
person = Person(given_names="Ada", last_name="Lovelace")
|
||||
tag = Tag(label="Pioneer", normalized_label="pioneer")
|
||||
session.add_all([person, tag])
|
||||
await session.flush()
|
||||
session.add(PersonTag(person_id=person.id, tag_id=tag.id))
|
||||
await session.commit()
|
||||
|
||||
response = client.get("/ui/people")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Lovelace" in response.text
|
||||
assert "Ada" in response.text
|
||||
assert "Last Name" in response.text
|
||||
assert "First & Middle" in response.text or "First & Middle" in response.text
|
||||
assert "Lovelace, Ada" in response.text
|
||||
assert "Last Name, First & Middle" in response.text or "Last Name, First & Middle" in response.text
|
||||
assert "Tags" in response.text
|
||||
assert "Pioneer" in response.text
|
||||
assert "FamilySearch ID" in response.text
|
||||
assert "# Documents" in response.text
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ class TestSourcesPageRendering:
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "job-failed-page.png" in response.text
|
||||
assert "failed" in response.text.lower()
|
||||
assert "FAILED" in response.text
|
||||
assert "Provider timed out" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user