generated from john/python-template
@@ -46,6 +46,11 @@ def _loaded_attribute(instance: object, attribute: str) -> Any | None:
|
|||||||
return state.dict.get(attribute)
|
return state.dict.get(attribute)
|
||||||
|
|
||||||
|
|
||||||
|
def _utc_now_naive() -> datetime:
|
||||||
|
"""Return a UTC timestamp stored as a naive datetime."""
|
||||||
|
return datetime.now(UTC).replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
class JSONBCompat(TypeDecorator):
|
class JSONBCompat(TypeDecorator):
|
||||||
"""JSONB for PostgreSQL and JSON for SQLite/testing backends."""
|
"""JSONB for PostgreSQL and JSON for SQLite/testing backends."""
|
||||||
|
|
||||||
@@ -87,10 +92,10 @@ class DocumentType(SQLModel, table=True):
|
|||||||
label: str
|
label: str
|
||||||
normalized_label: str = Field(index=True, unique=True)
|
normalized_label: str = Field(index=True, unique=True)
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
created_at: datetime = Field(default_factory=_utc_now_naive)
|
||||||
updated_at: datetime = Field(
|
updated_at: datetime = Field(
|
||||||
default_factory=lambda: datetime.now(UTC),
|
default_factory=_utc_now_naive,
|
||||||
sa_column_kwargs={"onupdate": lambda: datetime.now(UTC)},
|
sa_column_kwargs={"onupdate": _utc_now_naive},
|
||||||
)
|
)
|
||||||
|
|
||||||
documents: list["Document"] = Relationship(
|
documents: list["Document"] = Relationship(
|
||||||
@@ -108,10 +113,10 @@ class PersonRole(SQLModel, table=True):
|
|||||||
label: str
|
label: str
|
||||||
normalized_label: str = Field(index=True, unique=True)
|
normalized_label: str = Field(index=True, unique=True)
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
created_at: datetime = Field(default_factory=_utc_now_naive)
|
||||||
updated_at: datetime = Field(
|
updated_at: datetime = Field(
|
||||||
default_factory=lambda: datetime.now(UTC),
|
default_factory=_utc_now_naive,
|
||||||
sa_column_kwargs={"onupdate": lambda: datetime.now(UTC)},
|
sa_column_kwargs={"onupdate": _utc_now_naive},
|
||||||
)
|
)
|
||||||
|
|
||||||
document_people: list["DocumentPerson"] = Relationship(
|
document_people: list["DocumentPerson"] = Relationship(
|
||||||
@@ -129,10 +134,10 @@ class Tag(SQLModel, table=True):
|
|||||||
label: str
|
label: str
|
||||||
normalized_label: str = Field(index=True, unique=True)
|
normalized_label: str = Field(index=True, unique=True)
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
created_at: datetime = Field(default_factory=_utc_now_naive)
|
||||||
updated_at: datetime = Field(
|
updated_at: datetime = Field(
|
||||||
default_factory=lambda: datetime.now(UTC),
|
default_factory=_utc_now_naive,
|
||||||
sa_column_kwargs={"onupdate": lambda: datetime.now(UTC)},
|
sa_column_kwargs={"onupdate": _utc_now_naive},
|
||||||
)
|
)
|
||||||
|
|
||||||
document_tags: list["DocumentTag"] = Relationship(
|
document_tags: list["DocumentTag"] = Relationship(
|
||||||
@@ -156,10 +161,10 @@ class Document(SQLModel, table=True):
|
|||||||
location_created: str | None = None
|
location_created: str | None = None
|
||||||
notes: str | None = None
|
notes: str | None = None
|
||||||
archive_identifier: str | None = None
|
archive_identifier: str | None = None
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
created_at: datetime = Field(default_factory=_utc_now_naive)
|
||||||
updated_at: datetime = Field(
|
updated_at: datetime = Field(
|
||||||
default_factory=lambda: datetime.now(UTC),
|
default_factory=_utc_now_naive,
|
||||||
sa_column_kwargs={"onupdate": lambda: datetime.now(UTC)},
|
sa_column_kwargs={"onupdate": _utc_now_naive},
|
||||||
)
|
)
|
||||||
|
|
||||||
jobs: list["Job"] = Relationship(back_populates="document", sa_relationship_kwargs={"lazy": "raise"})
|
jobs: list["Job"] = Relationship(back_populates="document", sa_relationship_kwargs={"lazy": "raise"})
|
||||||
@@ -194,10 +199,10 @@ class Person(SQLModel, table=True):
|
|||||||
default=None,
|
default=None,
|
||||||
sa_column=Column("metadata", JSONBCompat(), nullable=True),
|
sa_column=Column("metadata", JSONBCompat(), nullable=True),
|
||||||
)
|
)
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
created_at: datetime = Field(default_factory=_utc_now_naive)
|
||||||
updated_at: datetime = Field(
|
updated_at: datetime = Field(
|
||||||
default_factory=lambda: datetime.now(UTC),
|
default_factory=_utc_now_naive,
|
||||||
sa_column_kwargs={"onupdate": lambda: datetime.now(UTC)},
|
sa_column_kwargs={"onupdate": _utc_now_naive},
|
||||||
)
|
)
|
||||||
|
|
||||||
document_people: list["DocumentPerson"] = Relationship(
|
document_people: list["DocumentPerson"] = Relationship(
|
||||||
@@ -228,10 +233,10 @@ class Photo(SQLModel, table=True):
|
|||||||
path: str
|
path: str
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
is_primary: bool = False
|
is_primary: bool = False
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
created_at: datetime = Field(default_factory=_utc_now_naive)
|
||||||
updated_at: datetime = Field(
|
updated_at: datetime = Field(
|
||||||
default_factory=lambda: datetime.now(UTC),
|
default_factory=_utc_now_naive,
|
||||||
sa_column_kwargs={"onupdate": lambda: datetime.now(UTC)},
|
sa_column_kwargs={"onupdate": _utc_now_naive},
|
||||||
)
|
)
|
||||||
|
|
||||||
person: Optional["Person"] = Relationship(
|
person: Optional["Person"] = Relationship(
|
||||||
@@ -249,10 +254,10 @@ class DocumentPerson(SQLModel, table=True):
|
|||||||
document_id: UUID = Field(foreign_key="document.id", index=True)
|
document_id: UUID = Field(foreign_key="document.id", index=True)
|
||||||
person_id: UUID = Field(foreign_key="person.id", index=True)
|
person_id: UUID = Field(foreign_key="person.id", index=True)
|
||||||
role_id: UUID = Field(foreign_key="person_role.id", index=True)
|
role_id: UUID = Field(foreign_key="person_role.id", index=True)
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
created_at: datetime = Field(default_factory=_utc_now_naive)
|
||||||
updated_at: datetime = Field(
|
updated_at: datetime = Field(
|
||||||
default_factory=lambda: datetime.now(UTC),
|
default_factory=_utc_now_naive,
|
||||||
sa_column_kwargs={"onupdate": lambda: datetime.now(UTC)},
|
sa_column_kwargs={"onupdate": _utc_now_naive},
|
||||||
)
|
)
|
||||||
|
|
||||||
__table_args__ = (UniqueConstraint("document_id", "person_id", name="uq_document_person"),)
|
__table_args__ = (UniqueConstraint("document_id", "person_id", name="uq_document_person"),)
|
||||||
@@ -276,10 +281,10 @@ class DocumentTag(SQLModel, table=True):
|
|||||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||||
document_id: UUID = Field(foreign_key="document.id", index=True)
|
document_id: UUID = Field(foreign_key="document.id", index=True)
|
||||||
tag_id: UUID = Field(foreign_key="tag.id", index=True)
|
tag_id: UUID = Field(foreign_key="tag.id", index=True)
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
created_at: datetime = Field(default_factory=_utc_now_naive)
|
||||||
updated_at: datetime = Field(
|
updated_at: datetime = Field(
|
||||||
default_factory=lambda: datetime.now(UTC),
|
default_factory=_utc_now_naive,
|
||||||
sa_column_kwargs={"onupdate": lambda: datetime.now(UTC)},
|
sa_column_kwargs={"onupdate": _utc_now_naive},
|
||||||
)
|
)
|
||||||
|
|
||||||
__table_args__ = (UniqueConstraint("document_id", "tag_id", name="uq_document_tag"),)
|
__table_args__ = (UniqueConstraint("document_id", "tag_id", name="uq_document_tag"),)
|
||||||
@@ -302,10 +307,10 @@ class PersonTag(SQLModel, table=True):
|
|||||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||||
person_id: UUID = Field(foreign_key="person.id", index=True)
|
person_id: UUID = Field(foreign_key="person.id", index=True)
|
||||||
tag_id: UUID = Field(foreign_key="tag.id", index=True)
|
tag_id: UUID = Field(foreign_key="tag.id", index=True)
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
created_at: datetime = Field(default_factory=_utc_now_naive)
|
||||||
updated_at: datetime = Field(
|
updated_at: datetime = Field(
|
||||||
default_factory=lambda: datetime.now(UTC),
|
default_factory=_utc_now_naive,
|
||||||
sa_column_kwargs={"onupdate": lambda: datetime.now(UTC)},
|
sa_column_kwargs={"onupdate": _utc_now_naive},
|
||||||
)
|
)
|
||||||
|
|
||||||
__table_args__ = (UniqueConstraint("person_id", "tag_id", name="uq_person_tag"),)
|
__table_args__ = (UniqueConstraint("person_id", "tag_id", name="uq_person_tag"),)
|
||||||
@@ -351,10 +356,10 @@ class Job(SQLModel, table=True):
|
|||||||
default=JobPurpose.TRANSCRIPTION.value,
|
default=JobPurpose.TRANSCRIPTION.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
date_created: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
date_created: datetime = Field(default_factory=_utc_now_naive)
|
||||||
date_updated: datetime = Field(
|
date_updated: datetime = Field(
|
||||||
default_factory=lambda: datetime.now(UTC),
|
default_factory=_utc_now_naive,
|
||||||
sa_column_kwargs={"onupdate": lambda: datetime.now(UTC)},
|
sa_column_kwargs={"onupdate": _utc_now_naive},
|
||||||
)
|
)
|
||||||
provider: str | None = None
|
provider: str | None = None
|
||||||
model: str | None = None
|
model: str | None = None
|
||||||
@@ -413,7 +418,7 @@ class Source(SQLModel, table=True):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
revised_text: str | None = None
|
revised_text: str | None = None
|
||||||
date_uploaded: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
date_uploaded: datetime = Field(default_factory=_utc_now_naive)
|
||||||
date_revised: datetime | None = None
|
date_revised: datetime | None = None
|
||||||
|
|
||||||
document: Optional["Document"] = Relationship(
|
document: Optional["Document"] = Relationship(
|
||||||
@@ -551,7 +556,7 @@ class ExecutionAttempt(SQLModel, table=True):
|
|||||||
started_at: datetime
|
started_at: datetime
|
||||||
finished_at: datetime
|
finished_at: datetime
|
||||||
duration_ms: int = Field(ge=0)
|
duration_ms: int = Field(ge=0)
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
created_at: datetime = Field(default_factory=_utc_now_naive)
|
||||||
|
|
||||||
job_source: Optional["JobSource"] = Relationship(
|
job_source: Optional["JobSource"] = Relationship(
|
||||||
back_populates="execution_attempts", sa_relationship_kwargs={"lazy": "raise"}
|
back_populates="execution_attempts", sa_relationship_kwargs={"lazy": "raise"}
|
||||||
|
|||||||
@@ -66,6 +66,18 @@ JSON_OBJECT_ADAPTER = TypeAdapter(dict[str, JsonValue])
|
|||||||
MAX_EXECUTION_ATTEMPT_NUMBER_RETRIES = 3
|
MAX_EXECUTION_ATTEMPT_NUMBER_RETRIES = 3
|
||||||
|
|
||||||
|
|
||||||
|
def _utc_now_naive() -> datetime:
|
||||||
|
"""Return current UTC as naive datetime for DB timestamp columns."""
|
||||||
|
return datetime.now(UTC).replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
|
def _as_naive_utc(value: datetime) -> datetime:
|
||||||
|
"""Normalize aware or naive datetimes to naive UTC."""
|
||||||
|
if value.tzinfo is None:
|
||||||
|
return value
|
||||||
|
return value.astimezone(UTC).replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
class PromptExecution(BaseModel):
|
class PromptExecution(BaseModel):
|
||||||
"""Resolved prompt inputs captured for one page execution."""
|
"""Resolved prompt inputs captured for one page execution."""
|
||||||
|
|
||||||
@@ -533,8 +545,8 @@ class SourceService(ServiceBase):
|
|||||||
else:
|
else:
|
||||||
job_source.status = outcome
|
job_source.status = outcome
|
||||||
|
|
||||||
finish_time = finished_at or datetime.now(UTC)
|
finish_time = _as_naive_utc(finished_at) if finished_at is not None else _utc_now_naive()
|
||||||
start_time = started_at or finish_time
|
start_time = _as_naive_utc(started_at) if started_at is not None else finish_time
|
||||||
transport = transport_evidence or TransportEvidence(response_received=False)
|
transport = transport_evidence or TransportEvidence(response_received=False)
|
||||||
manifest_payload = request_manifest.model_dump(mode="json") if request_manifest is not None else None
|
manifest_payload = request_manifest.model_dump(mode="json") if request_manifest is not None else None
|
||||||
software_payload = (
|
software_payload = (
|
||||||
@@ -641,7 +653,7 @@ class SourceService(ServiceBase):
|
|||||||
source = await self._read_source(session=_session, source_id=source_id)
|
source = await self._read_source(session=_session, source_id=source_id)
|
||||||
|
|
||||||
source.revised_text = text
|
source.revised_text = text
|
||||||
source.date_revised = datetime.now(UTC)
|
source.date_revised = _utc_now_naive()
|
||||||
await self._finalize(session=_session, caller_session=session, refresh=(source,))
|
await self._finalize(session=_session, caller_session=session, refresh=(source,))
|
||||||
return source
|
return source
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ _RETRIABLE_FAILED_JOB_ERROR_CATEGORIES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _utc_now_naive() -> datetime:
|
||||||
|
"""Return current UTC as naive datetime for DB timestamp columns."""
|
||||||
|
return datetime.now(UTC).replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
async def create_document_with_people(
|
async def create_document_with_people(
|
||||||
*,
|
*,
|
||||||
document: Document,
|
document: Document,
|
||||||
@@ -272,7 +277,7 @@ async def process_queued_job( # noqa: PLR0915
|
|||||||
externally_stopped = True
|
externally_stopped = True
|
||||||
break
|
break
|
||||||
|
|
||||||
started_at = datetime.now(UTC)
|
started_at = _utc_now_naive()
|
||||||
# Fallback start for failures raised before the provider call; reset to the
|
# Fallback start for failures raised before the provider call; reset to the
|
||||||
# true call boundary immediately before the wait_for below.
|
# true call boundary immediately before the wait_for below.
|
||||||
monotonic_started_at = asyncio.get_running_loop().time()
|
monotonic_started_at = asyncio.get_running_loop().time()
|
||||||
@@ -327,7 +332,7 @@ async def process_queued_job( # noqa: PLR0915
|
|||||||
)
|
)
|
||||||
|
|
||||||
_validate_transcription_quality(result=result, settings=runtime_settings)
|
_validate_transcription_quality(result=result, settings=runtime_settings)
|
||||||
finished_at = datetime.now(UTC)
|
finished_at = _utc_now_naive()
|
||||||
page_outcome = _SuccessfulPage(
|
page_outcome = _SuccessfulPage(
|
||||||
source=source,
|
source=source,
|
||||||
result=result,
|
result=result,
|
||||||
@@ -347,7 +352,7 @@ async def process_queued_job( # noqa: PLR0915
|
|||||||
suggestion="Retry the job. If this repeats, verify provider latency and request payload size.",
|
suggestion="Retry the job. If this repeats, verify provider latency and request payload size.",
|
||||||
retriable=True,
|
retriable=True,
|
||||||
)
|
)
|
||||||
finished_at = datetime.now(UTC)
|
finished_at = _utc_now_naive()
|
||||||
page_outcome = _FailedPage(
|
page_outcome = _FailedPage(
|
||||||
source=source,
|
source=source,
|
||||||
error=error,
|
error=error,
|
||||||
@@ -382,7 +387,7 @@ async def process_queued_job( # noqa: PLR0915
|
|||||||
case _:
|
case _:
|
||||||
error = classify_unexpected_error(exc, operation="worker.process_job")
|
error = classify_unexpected_error(exc, operation="worker.process_job")
|
||||||
|
|
||||||
finished_at = datetime.now(UTC)
|
finished_at = _utc_now_naive()
|
||||||
provider_error = _find_provider_error(exc)
|
provider_error = _find_provider_error(exc)
|
||||||
page_outcome = _FailedPage(
|
page_outcome = _FailedPage(
|
||||||
source=source,
|
source=source,
|
||||||
|
|||||||
@@ -541,11 +541,21 @@ def _render_document_form_fields(
|
|||||||
|
|
||||||
def _render_bento_viewer_zone(document: Document, *, base_url: str, settings: Settings) -> None:
|
def _render_bento_viewer_zone(document: Document, *, base_url: str, settings: Settings) -> None:
|
||||||
with ui.column().classes("col-span-12 lg:col-span-4"):
|
with ui.column().classes("col-span-12 lg:col-span-4"):
|
||||||
source_path = document.sources[0].file_path if document.sources else None
|
source_path = _first_source_path(document)
|
||||||
source_url = resolve_media_url(source_path, upload_dir=settings.upload_dir, base_url=base_url)
|
source_url = resolve_media_url(source_path, upload_dir=settings.upload_dir, base_url=base_url)
|
||||||
dark_room_viewer(source_url, count_label=f"{len(document.sources)} Source(s) Linked")
|
dark_room_viewer(source_url, count_label=f"{len(document.sources)} Source(s) Linked")
|
||||||
|
|
||||||
|
|
||||||
|
def _first_source_path(document: Document) -> str | None:
|
||||||
|
if not document.sources:
|
||||||
|
return None
|
||||||
|
first_source = min(
|
||||||
|
document.sources,
|
||||||
|
key=lambda source: (source.page_number, source.upload_name.casefold(), source.filename.casefold()),
|
||||||
|
)
|
||||||
|
return first_source.file_path
|
||||||
|
|
||||||
|
|
||||||
def _render_bento_metadata_zone(document: Document) -> None:
|
def _render_bento_metadata_zone(document: Document) -> None:
|
||||||
author_names = _author_names(document)
|
author_names = _author_names(document)
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ class TestDocumentModel:
|
|||||||
assert isinstance(document.id, UUID)
|
assert isinstance(document.id, UUID)
|
||||||
assert document.created_at is not None
|
assert document.created_at is not None
|
||||||
assert document.updated_at is not None
|
assert document.updated_at is not None
|
||||||
|
assert document.created_at.tzinfo is None
|
||||||
|
assert document.updated_at.tzinfo is None
|
||||||
|
|
||||||
def test_can_reference_document_type_registry(self, session):
|
def test_can_reference_document_type_registry(self, session):
|
||||||
document_type = _persist_document_type(session, label="Record")
|
document_type = _persist_document_type(session, label="Record")
|
||||||
@@ -140,6 +142,8 @@ class TestJobModel:
|
|||||||
assert job.retry_count == 0
|
assert job.retry_count == 0
|
||||||
assert job.date_created is not None
|
assert job.date_created is not None
|
||||||
assert job.date_updated is not None
|
assert job.date_updated is not None
|
||||||
|
assert job.date_created.tzinfo is None
|
||||||
|
assert job.date_updated.tzinfo is None
|
||||||
|
|
||||||
def test_transitions_to_transcribed(self, session):
|
def test_transitions_to_transcribed(self, session):
|
||||||
document = _persist_document(session)
|
document = _persist_document(session)
|
||||||
@@ -183,6 +187,7 @@ class TestSourceModel:
|
|||||||
assert fetched.document_id == document.id
|
assert fetched.document_id == document.id
|
||||||
assert fetched.page_number == 1
|
assert fetched.page_number == 1
|
||||||
assert fetched.date_uploaded is not None
|
assert fetched.date_uploaded is not None
|
||||||
|
assert fetched.date_uploaded.tzinfo is None
|
||||||
|
|
||||||
def test_revised_text_is_supported(self, session):
|
def test_revised_text_is_supported(self, session):
|
||||||
document = _persist_document(session)
|
document = _persist_document(session)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from transcription.db.models import Person
|
|||||||
from transcription.db.models import PersonRole
|
from transcription.db.models import PersonRole
|
||||||
from transcription.db.models import Source
|
from transcription.db.models import Source
|
||||||
from transcription.db.models import Tag
|
from transcription.db.models import Tag
|
||||||
|
from transcription.ui.pages.documents_page import _first_source_path
|
||||||
from transcription.ui.pages.documents_page import _resolve_selected_tag_labels
|
from transcription.ui.pages.documents_page import _resolve_selected_tag_labels
|
||||||
|
|
||||||
# --- Helper Fixtures ---
|
# --- Helper Fixtures ---
|
||||||
@@ -107,6 +108,40 @@ class TestDocumentsPageRendering:
|
|||||||
re.DOTALL,
|
re.DOTALL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_first_source_path_prefers_lowest_page_number(self):
|
||||||
|
document = Document(name="Ordering Test")
|
||||||
|
document.sources = [
|
||||||
|
Source(
|
||||||
|
document_id=document.id,
|
||||||
|
page_number=3,
|
||||||
|
upload_name="c.jpg",
|
||||||
|
filename="c.jpg",
|
||||||
|
file_path="documents/c.jpg",
|
||||||
|
file_hash="c" * 64,
|
||||||
|
file_size_bytes=1,
|
||||||
|
),
|
||||||
|
Source(
|
||||||
|
document_id=document.id,
|
||||||
|
page_number=1,
|
||||||
|
upload_name="a.jpg",
|
||||||
|
filename="a.jpg",
|
||||||
|
file_path="documents/a.jpg",
|
||||||
|
file_hash="a" * 64,
|
||||||
|
file_size_bytes=1,
|
||||||
|
),
|
||||||
|
Source(
|
||||||
|
document_id=document.id,
|
||||||
|
page_number=2,
|
||||||
|
upload_name="b.jpg",
|
||||||
|
filename="b.jpg",
|
||||||
|
file_path="documents/b.jpg",
|
||||||
|
file_hash="b" * 64,
|
||||||
|
file_size_bytes=1,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
assert _first_source_path(document) == "documents/a.jpg"
|
||||||
|
|
||||||
def test_document_create_page_renders_form(self, app_client):
|
def test_document_create_page_renders_form(self, app_client):
|
||||||
_, client = app_client
|
_, client = app_client
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user