Compare commits

..
57 changed files with 2111 additions and 780 deletions

No files matched your search

-1
View File
@@ -38,7 +38,6 @@ src/documents/bulk_edit.py:0: error: Incompatible types in assignment (expressio
src/documents/bulk_edit.py:0: error: Invalid index type "str" for "dict[FieldDataType, str]"; expected type "FieldDataType" [index] src/documents/bulk_edit.py:0: error: Invalid index type "str" for "dict[FieldDataType, str]"; expected type "FieldDataType" [index]
src/documents/bulk_edit.py:0: error: List comprehension has incompatible type List[tuple[int, Any]]; expected List[int] [misc] src/documents/bulk_edit.py:0: error: List comprehension has incompatible type List[tuple[int, Any]]; expected List[int] [misc]
src/documents/bulk_edit.py:0: error: List comprehension has incompatible type List[tuple[int, None]]; expected List[int] [misc] src/documents/bulk_edit.py:0: error: List comprehension has incompatible type List[tuple[int, None]]; expected List[int] [misc]
src/documents/bulk_edit.py:0: error: Missing named argument "p" for "remove" of "PageList" [call-arg]
src/documents/bulk_edit.py:0: error: Missing type arguments for generic type "dict" [type-arg] src/documents/bulk_edit.py:0: error: Missing type arguments for generic type "dict" [type-arg]
src/documents/bulk_edit.py:0: error: Missing type arguments for generic type "dict" [type-arg] src/documents/bulk_edit.py:0: error: Missing type arguments for generic type "dict" [type-arg]
src/documents/bulk_edit.py:0: error: Need type annotation for "to_create" (hint: "to_create: list[<type>] = ...") [var-annotated] src/documents/bulk_edit.py:0: error: Need type annotation for "to_create" (hint: "to_create: list[<type>] = ...") [var-annotated]
-7
View File
@@ -91,13 +91,6 @@
"concise_description": "Argument `list[int]` is not assignable to parameter `args` with type `tuple[Any, ...] | None` in function `celery.app.task.Task.apply_async`", "concise_description": "Argument `list[int]` is not assignable to parameter `args` with type `tuple[Any, ...] | None` in function `celery.app.task.Task.apply_async`",
"severity": "error" "severity": "error"
}, },
{
"column": 33,
"path": "src/documents/bulk_edit.py",
"name": "missing-argument",
"concise_description": "Missing argument `p` in function `pikepdf._core.PageList.remove`",
"severity": "error"
},
{ {
"column": 25, "column": 25,
"path": "src/documents/caching.py", "path": "src/documents/caching.py",
+1 -1
View File
@@ -8525,7 +8525,7 @@
<context context-type="sourcefile">src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html</context>
<context context-type="linenumber">54</context> <context context-type="linenumber">54</context>
</context-group> </context-group>
<target state="translated">{VAR_PLURAL, plural, =1 {1 Vorschlag unten verfügbar} other {<x id="INTERPOLATION"/> Vorschläge unten verfügbar}}</target> <target state="needs-translation">{VAR_PLURAL, plural, =1 {1 suggestion available below} other {<x id="INTERPOLATION"/> suggestions available below}}</target>
</trans-unit> </trans-unit>
<trans-unit id="4169875112244042673" datatype="html" approved="yes"> <trans-unit id="4169875112244042673" datatype="html" approved="yes">
<source>Suggestion options</source> <source>Suggestion options</source>
File diff suppressed because it is too large. Load diff
+184 -219
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import logging import logging
import tempfile import tempfile
import uuid import uuid
from functools import partial
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Literal from typing import Literal
@@ -17,6 +18,7 @@ from django.db.models import Max
from django.db.models import Q from django.db.models import Q
from django.utils import timezone from django.utils import timezone
from documents import pdf_ops
from documents.data_models import ConsumableDocument from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides from documents.data_models import DocumentMetadataOverrides
from documents.data_models import DocumentSource from documents.data_models import DocumentSource
@@ -116,6 +118,11 @@ def _resolve_root_and_source_doc(
) )
def _scratch_path(name: str) -> Path:
"""A path inside a fresh directory under SCRATCH_DIR."""
return Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR)) / name
def set_correspondent( def set_correspondent(
doc_ids: list[int], doc_ids: list[int],
correspondent: Correspondent, correspondent: Correspondent,
@@ -474,8 +481,6 @@ def rotate(
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode) pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
docs_by_root_id.setdefault(pair.root_doc.id, pair) docs_by_root_id.setdefault(pair.root_doc.id, pair)
import pikepdf
for pair in docs_by_root_id.values(): for pair in docs_by_root_id.values():
if pair.source_doc.mime_type != "application/pdf": if pair.source_doc.mime_type != "application/pdf":
logger.warning( logger.warning(
@@ -488,11 +493,7 @@ def rotate(
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR)) Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
/ f"{pair.root_doc.id}_rotated.pdf" / f"{pair.root_doc.id}_rotated.pdf"
) )
with pikepdf.open(pair.source_doc.source_path) as pdf: pdf_ops.rotate_pdf(pair.source_doc.source_path, filepath, degrees)
for page in pdf.pages:
page.rotate(degrees, relative=True)
pdf.remove_unreferenced_resources()
pdf.save(filepath)
# Preserve metadata/permissions via overrides; mark as new version # Preserve metadata/permissions via overrides; mark as new version
overrides = DocumentMetadataOverrides().from_document(pair.root_doc) overrides = DocumentMetadataOverrides().from_document(pair.root_doc)
@@ -535,48 +536,45 @@ def merge(
qs = Document.objects.select_related("root_document").filter(id__in=doc_ids) qs = Document.objects.select_related("root_document").filter(id__in=doc_ids)
docs_by_id = {doc.id: doc for doc in qs} docs_by_id = {doc.id: doc for doc in qs}
affected_docs: list[int] = [] affected_docs: list[int] = []
import pikepdf
merged_pdf = pikepdf.new()
version: str = merged_pdf.pdf_version
handoff_asn: int | None = None handoff_asn: int | None = None
# use doc_ids to preserve order with pdf_ops.PdfMerger() as merger:
for doc_id in doc_ids: # use doc_ids to preserve order
doc = docs_by_id.get(doc_id) for doc_id in doc_ids:
if doc is None: doc = docs_by_id.get(doc_id)
continue if doc is None:
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode) continue
try: pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
doc_path = ( try:
pair.source_doc.archive_path # archive_path is None when there is no archive version
if archive_fallback archive_path = (
and pair.source_doc.mime_type != "application/pdf" pair.source_doc.archive_path
and pair.source_doc.has_archive_version if archive_fallback
else pair.source_doc.source_path and pair.source_doc.mime_type != "application/pdf"
) else None
with pikepdf.open(str(doc_path)) as pdf: )
version = max(version, pdf.pdf_version) merger.add(
merged_pdf.pages.extend(pdf.pages) archive_path
affected_docs.append(doc.id) if archive_path is not None
if handoff_asn is None and doc.archive_serial_number is not None: else pair.source_doc.source_path,
handoff_asn = doc.archive_serial_number )
except Exception as e: affected_docs.append(doc.id)
logger.exception( if handoff_asn is None and doc.archive_serial_number is not None:
f"Error merging document {doc.id}, it will not be included in the merge: {e}", handoff_asn = doc.archive_serial_number
) except Exception as e:
if len(affected_docs) == 0: logger.exception(
logger.warning("No documents were merged") f"Error merging document {doc.id}, it will not be included in the merge: {e}",
return "OK" )
if len(affected_docs) == 0:
logger.warning("No documents were merged")
return "OK"
filepath = ( filepath = (
Path( Path(
tempfile.mkdtemp(dir=settings.SCRATCH_DIR), tempfile.mkdtemp(dir=settings.SCRATCH_DIR),
)
/ f"{'_'.join([str(doc_id) for doc_id in affected_docs])[:100]}_merged.pdf"
) )
/ f"{'_'.join([str(doc_id) for doc_id in affected_docs])[:100]}_merged.pdf" merger.save(filepath)
)
merged_pdf.remove_unreferenced_resources()
merged_pdf.save(filepath, min_version=version)
merged_pdf.close()
if metadata_document_id: if metadata_document_id:
metadata_document = qs.get(id=metadata_document_id) metadata_document = qs.get(id=metadata_document_id)
@@ -752,64 +750,60 @@ def split(
) )
doc = Document.objects.select_related("root_document").get(id=doc_ids[0]) doc = Document.objects.select_related("root_document").get(id=doc_ids[0])
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode) pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
import pikepdf
consume_tasks = [] consume_tasks = []
try: try:
with pikepdf.open(pair.source_doc.source_path) as pdf: outputs = [
for idx, split_doc in enumerate(pages): (
dst: pikepdf.Pdf = pikepdf.new() [pdf_ops.PageSpec(page) for page in split_doc],
for page in split_doc: partial(_scratch_path, f"{doc.id}_{split_doc[0]}-{split_doc[-1]}.pdf"),
dst.pages.append(pdf.pages[page - 1]) )
filepath: Path = ( for split_doc in pages
Path( ]
tempfile.mkdtemp(dir=settings.SCRATCH_DIR), filepaths = pdf_ops.build_pdfs(pair.source_doc.source_path, outputs)
)
/ f"{doc.id}_{split_doc[0]}-{split_doc[-1]}.pdf"
)
dst.remove_unreferenced_resources()
dst.save(filepath)
dst.close()
overrides: DocumentMetadataOverrides = ( for idx, (split_doc, filepath) in enumerate(
DocumentMetadataOverrides().from_document(doc) zip(pages, filepaths, strict=True),
) ):
overrides.title = f"{doc.title} (split {idx + 1})" overrides: DocumentMetadataOverrides = (
if user is not None: DocumentMetadataOverrides().from_document(doc)
overrides.owner_id = user.id )
if not delete_originals: overrides.title = f"{doc.title} (split {idx + 1})"
overrides.skip_asn_if_exists = True if user is not None:
logger.info( overrides.owner_id = user.id
f"Adding split document with pages {split_doc} to the task queue.", if not delete_originals:
) overrides.skip_asn_if_exists = True
consume_tasks.append( logger.info(
consume_file.s( f"Adding split document with pages {split_doc} to the task queue.",
input_doc=ConsumableDocument( )
source=DocumentSource.ConsumeFolder, consume_tasks.append(
original_file=filepath, consume_file.s(
), input_doc=ConsumableDocument(
overrides=overrides, source=DocumentSource.ConsumeFolder,
).set(headers={"trigger_source": trigger_source}), original_file=filepath,
) ),
overrides=overrides,
).set(headers={"trigger_source": trigger_source}),
)
if delete_originals: if delete_originals:
backup = release_archive_serial_numbers([doc.id]) backup = release_archive_serial_numbers([doc.id])
logger.info( logger.info(
"Queueing removal of original document after consumption of the split documents", "Queueing removal of original document after consumption of the split documents",
) )
try: try:
chord( chord(
header=consume_tasks, header=consume_tasks,
body=delete.si([doc.id]), body=delete.si([doc.id]),
).on_error( ).on_error(
restore_archive_serial_numbers_task.s(backup), restore_archive_serial_numbers_task.s(backup),
).apply_async() ).apply_async()
except Exception: except Exception:
restore_archive_serial_numbers(backup) restore_archive_serial_numbers(backup)
raise raise
else: else:
group(consume_tasks).delay() group(consume_tasks).delay()
except Exception as e: except Exception as e:
logger.exception(f"Error splitting document {doc.id}: {e}") logger.exception(f"Error splitting document {doc.id}: {e}")
@@ -830,8 +824,6 @@ def delete_pages(
) )
doc = Document.objects.select_related("root_document").get(id=doc_ids[0]) doc = Document.objects.select_related("root_document").get(id=doc_ids[0])
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode) pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
pages = sorted(pages) # sort pages to avoid index issues
import pikepdf
try: try:
# Produce edited PDF to a temp file and create a new version # Produce edited PDF to a temp file and create a new version
@@ -839,13 +831,7 @@ def delete_pages(
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR)) Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
/ f"{pair.root_doc.id}_pages_deleted.pdf" / f"{pair.root_doc.id}_pages_deleted.pdf"
) )
with pikepdf.open(pair.source_doc.source_path) as pdf: pdf_ops.remove_pages(pair.source_doc.source_path, filepath, pages)
offset = 1 # pages are 1-indexed
for page_num in pages:
pdf.pages.remove(pdf.pages[page_num - offset])
offset += 1 # remove() changes the index of the pages
pdf.remove_unreferenced_resources()
pdf.save(filepath)
overrides = DocumentMetadataOverrides().from_document(pair.root_doc) overrides = DocumentMetadataOverrides().from_document(pair.root_doc)
if user is not None: if user is not None:
@@ -894,47 +880,28 @@ def edit_pdf(
) )
doc = Document.objects.select_related("root_document").get(id=doc_ids[0]) doc = Document.objects.select_related("root_document").get(id=doc_ids[0])
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode) pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
import pikepdf
pdf_docs: list[pikepdf.Pdf] = []
try: try:
if not operations: output_count = pdf_ops.validate_page_operations(
raise ValueError("Output document index is out of bounds") operations,
single_output=update_document,
max_idx = max(op.get("doc", 0) for op in operations) )
if update_document and max_idx > 0: page_specs: list[list[pdf_ops.PageSpec]] = [[] for _ in range(output_count)]
logger.error( for op in operations:
"Update requested but multiple output documents specified", page_specs[op.get("doc", 0)].append(
pdf_ops.PageSpec(op["page"], op.get("rotate", 0)),
) )
raise ValueError("Multiple output documents specified")
if any(
op.get("doc", 0) < 0 or op.get("doc", 0) >= len(operations)
for op in operations
):
raise ValueError("Output document index is out of bounds")
with pikepdf.open(pair.source_doc.source_path) as src:
# prepare output documents
pdf_docs = [pikepdf.new() for _ in range(max_idx + 1)]
for op in operations:
dst = pdf_docs[op.get("doc", 0)]
page = src.pages[op["page"] - 1]
dst.pages.append(page)
if op.get("rotate"):
dst.pages[-1].rotate(op["rotate"], relative=True)
if update_document: if update_document:
# Create a new version from the edited PDF rather than replacing in-place # Create a new version from the edited PDF rather than replacing in-place
pdf = pdf_docs[0] (filepath,) = pdf_ops.build_pdfs(
pdf.remove_unreferenced_resources() pair.source_doc.source_path,
filepath: Path = ( [
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR)) (
/ f"{pair.root_doc.id}_edited.pdf" page_specs[0],
partial(_scratch_path, f"{pair.root_doc.id}_edited.pdf"),
),
],
) )
pdf.save(filepath)
overrides = ( overrides = (
DocumentMetadataOverrides().from_document(pair.root_doc) DocumentMetadataOverrides().from_document(pair.root_doc)
if include_metadata if include_metadata
@@ -955,6 +922,19 @@ def edit_pdf(
headers={"trigger_source": trigger_source}, headers={"trigger_source": trigger_source},
) )
else: else:
version_filepaths = pdf_ops.build_pdfs(
pair.source_doc.source_path,
[
(
specs,
partial(
_scratch_path,
f"{pair.root_doc.id}_edit_{idx}.pdf",
),
)
for idx, specs in enumerate(page_specs, start=1)
],
)
consume_tasks = [] consume_tasks = []
overrides = ( overrides = (
DocumentMetadataOverrides().from_document(pair.root_doc) DocumentMetadataOverrides().from_document(pair.root_doc)
@@ -966,15 +946,9 @@ def edit_pdf(
overrides.actor_id = user.id overrides.actor_id = user.id
if not delete_original: if not delete_original:
overrides.skip_asn_if_exists = True overrides.skip_asn_if_exists = True
if delete_original and len(pdf_docs) == 1: if delete_original and output_count == 1:
overrides.asn = pair.root_doc.archive_serial_number overrides.asn = pair.root_doc.archive_serial_number
for idx, pdf in enumerate(pdf_docs, start=1): for version_filepath in version_filepaths:
version_filepath: Path = (
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
/ f"{pair.root_doc.id}_edit_{idx}.pdf"
)
pdf.remove_unreferenced_resources()
pdf.save(version_filepath)
consume_tasks.append( consume_tasks.append(
consume_file.s( consume_file.s(
input_doc=ConsumableDocument( input_doc=ConsumableDocument(
@@ -1024,8 +998,6 @@ def remove_password(
""" """
Remove password protection from PDF documents. Remove password protection from PDF documents.
""" """
import pikepdf
for doc_id in doc_ids: for doc_id in doc_ids:
doc = Document.objects.select_related("root_document").get(id=doc_id) doc = Document.objects.select_related("root_document").get(id=doc_id)
pair = _resolve_root_and_source_doc(doc, source_mode=source_mode) pair = _resolve_root_and_source_doc(doc, source_mode=source_mode)
@@ -1039,76 +1011,69 @@ def remove_password(
doc.id, doc.id,
pair.source_doc.source_path, pair.source_doc.source_path,
) )
try: if not pdf_ops.needs_decrypt(source_path):
with pikepdf.open(source_path) as pdf: logger.info(
if not pdf.is_encrypted: "Skipping password removal for document %s because the "
logger.info( "source PDF is not encrypted",
"Skipping password removal for document %s because the " pair.root_doc.id,
"source PDF is not encrypted",
pair.root_doc.id,
)
continue
except pikepdf.PasswordError:
# Password-protected PDFs need the supplied password below.
pass
with pikepdf.open(source_path, password=password) as pdf:
filepath: Path = (
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
/ f"{pair.root_doc.id}_unprotected.pdf"
) )
pdf.remove_unreferenced_resources() continue
pdf.save(filepath)
if update_document: filepath = pdf_ops.decrypt_pdf(
# Create a new version rather than modifying the root/original in place. source_path,
overrides = ( partial(_scratch_path, f"{pair.root_doc.id}_unprotected.pdf"),
DocumentMetadataOverrides().from_document(pair.root_doc) password,
if include_metadata )
else DocumentMetadataOverrides()
) if update_document:
if user is not None: # Create a new version rather than modifying the root/original in place.
overrides.owner_id = user.id overrides = (
overrides.actor_id = user.id DocumentMetadataOverrides().from_document(pair.root_doc)
consume_file.apply_async( if include_metadata
kwargs={ else DocumentMetadataOverrides()
"input_doc": ConsumableDocument( )
source=DocumentSource.ConsumeFolder, if user is not None:
original_file=filepath, overrides.owner_id = user.id
root_document_id=pair.root_doc.id, overrides.actor_id = user.id
), consume_file.apply_async(
"overrides": overrides, kwargs={
}, "input_doc": ConsumableDocument(
headers={"trigger_source": trigger_source}, source=DocumentSource.ConsumeFolder,
) original_file=filepath,
root_document_id=pair.root_doc.id,
),
"overrides": overrides,
},
headers={"trigger_source": trigger_source},
)
else:
consume_tasks = []
overrides = (
DocumentMetadataOverrides().from_document(pair.root_doc)
if include_metadata
else DocumentMetadataOverrides()
)
if user is not None:
overrides.owner_id = user.id
overrides.actor_id = user.id
consume_tasks.append(
consume_file.s(
input_doc=ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=filepath,
),
overrides=overrides,
).set(headers={"trigger_source": trigger_source}),
)
if delete_original:
chord(
header=consume_tasks,
body=delete.si([doc.id]),
).delay()
else: else:
consume_tasks = [] group(consume_tasks).delay()
overrides = (
DocumentMetadataOverrides().from_document(pair.root_doc)
if include_metadata
else DocumentMetadataOverrides()
)
if user is not None:
overrides.owner_id = user.id
overrides.actor_id = user.id
consume_tasks.append(
consume_file.s(
input_doc=ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=filepath,
),
overrides=overrides,
).set(headers={"trigger_source": trigger_source}),
)
if delete_original:
chord(
header=consume_tasks,
body=delete.si([doc.id]),
).delay()
else:
group(consume_tasks).delay()
except Exception as e: except Exception as e:
logger.exception( logger.exception(
+194
View File
@@ -0,0 +1,194 @@
"""
Pure PDF page operations used by documents.bulk_edit.
This module deliberately knows nothing about Django, Celery or the documents
app: callers resolve documents, choose output paths and queue work. Every
function that writes a PDF removes unreferenced resources before saving.
pikepdf is always called as ``pikepdf.open(...)`` / ``pikepdf.new()`` (never
``from pikepdf import open``) so tests can patch those module attributes.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import NamedTuple
import pikepdf
if TYPE_CHECKING:
from collections.abc import Callable
from collections.abc import Iterable
from collections.abc import Mapping
from collections.abc import Sequence
from pathlib import Path
from types import TracebackType
class PageSpec(NamedTuple):
"""One page of an output PDF: a 1-indexed source page, optionally rotated."""
page: int
rotate: int = 0 # relative degrees, 0 leaves the page alone
def _require_positive(pages: Iterable[int]) -> None:
for page in pages:
if page < 1:
raise ValueError(f"Page numbers start at 1, got {page}")
def rotate_pdf(src: Path, dst: Path, degrees: int) -> None:
"""
Rotate every page relatively on the opened document, not a rebuild, so Info,
XMP and outlines are kept. ``src`` is not modified.
"""
with pikepdf.open(src) as pdf:
for page in pdf.pages:
page.rotate(degrees, relative=True)
pdf.remove_unreferenced_resources()
pdf.save(dst)
def remove_pages(src: Path, dst: Path, pages: Iterable[int]) -> None:
"""
Remove 1-indexed pages from the opened document, not a rebuild, so Info, XMP
and outlines are kept. ``src`` is not modified.
Duplicates are ignored. Pages are removed highest first so earlier removals
never shift the index of later ones.
"""
unique = sorted(set(pages))
_require_positive(unique)
with pikepdf.open(src) as pdf:
for page_num in reversed(unique):
del pdf.pages[page_num - 1]
pdf.remove_unreferenced_resources()
pdf.save(dst)
def build_pdfs(
src: Path,
outputs: Sequence[tuple[Sequence[PageSpec], Callable[[], Path]]],
) -> list[Path]:
"""
Build one new PDF per output from pages of ``src``, opening ``src`` once.
Each output is ``(page_specs, make_dst)``. Every page number is checked against
``src`` before any output is built, and ``make_dst`` is called after that
output's pages are copied and immediately before it is saved, so a bad page
number in any output never leaves a destination behind. Document-level data
(Info, XMP, outlines) is not carried over. Returns the written paths in output
order.
"""
for specs, _ in outputs:
_require_positive(spec.page for spec in specs)
written: list[Path] = []
with pikepdf.open(src) as source:
page_count = len(source.pages)
for specs, _ in outputs:
for spec in specs:
if spec.page > page_count:
raise IndexError(
f"Page {spec.page} is out of range, the PDF has "
f"{page_count} pages",
)
for specs, make_dst in outputs:
dst = pikepdf.new()
for spec in specs:
dst.pages.append(source.pages[spec.page - 1])
if spec.rotate:
dst.pages[-1].rotate(spec.rotate, relative=True)
dst.remove_unreferenced_resources()
path = make_dst()
dst.save(path)
dst.close()
written.append(path)
return written
def validate_page_operations(
operations: Sequence[Mapping[str, int]],
*,
single_output: bool,
) -> int:
"""
Validate ``edit_pdf`` style operations and return the output document count.
Each operation has ``page`` and optionally ``rotate`` and ``doc`` (the output
document index, default 0). The bounds rule is kept as it was: a ``doc`` index
must be below the number of operations.
"""
if not operations:
raise ValueError("Output document index is out of bounds")
max_idx = max(op.get("doc", 0) for op in operations)
if single_output and max_idx > 0:
raise ValueError("Multiple output documents specified")
if any(
op.get("doc", 0) < 0 or op.get("doc", 0) >= len(operations) for op in operations
):
raise ValueError("Output document index is out of bounds")
return max_idx + 1
def needs_decrypt(src: Path) -> bool:
"""
True if ``src`` is encrypted. A PDF that needs a password to open at all
counts as encrypted.
"""
try:
with pikepdf.open(src) as pdf:
return bool(pdf.is_encrypted)
except pikepdf.PasswordError:
return True
def decrypt_pdf(src: Path, make_dst: Callable[[], Path], password: str) -> Path:
"""
Write an unencrypted copy of ``src`` and return its path.
``make_dst`` is only called once the password has been accepted, so a wrong
password never leaves a destination behind.
"""
with pikepdf.open(src, password=password) as pdf:
pdf.remove_unreferenced_resources()
dst = make_dst()
pdf.save(dst)
return dst
class PdfMerger:
"""
Accumulates the pages of several PDFs into one new PDF.
``add`` raises if a source cannot be read; deciding whether to skip it is the
caller's policy. Use as a context manager so the merged PDF is closed.
"""
def __init__(self) -> None:
self._pdf = pikepdf.new()
self._version: str = self._pdf.pdf_version
def __enter__(self) -> PdfMerger:
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
self._pdf.close()
def add(self, path: Path) -> None:
with pikepdf.open(str(path)) as pdf:
self._version = max(self._version, pdf.pdf_version)
self._pdf.pages.extend(pdf.pages)
def save(self, dst: Path) -> None:
self._pdf.remove_unreferenced_resources()
self._pdf.save(dst, min_version=self._version)
+2
View File
@@ -2144,6 +2144,8 @@ class BulkEditSerializer(
raise serializers.ValidationError("pages must be a list") raise serializers.ValidationError("pages must be a list")
if not all(isinstance(i, int) for i in parameters["pages"]): if not all(isinstance(i, int) for i in parameters["pages"]):
raise serializers.ValidationError("pages must be a list of integers") raise serializers.ValidationError("pages must be a list of integers")
if any(i < 1 for i in parameters["pages"]):
raise serializers.ValidationError("pages must be positive integers")
def _validate_parameters_merge(self, parameters) -> None: def _validate_parameters_merge(self, parameters) -> None:
if "delete_originals" in parameters: if "delete_originals" in parameters:
+30
View File
@@ -1843,6 +1843,36 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
m.assert_called_once() m.assert_called_once()
self.assertEqual(m.call_args.kwargs["pages"], [[1], [2, 3, 4], [5]]) self.assertEqual(m.call_args.kwargs["pages"], [[1], [2, 3, 4], [5]])
@mock.patch("documents.serialisers.bulk_edit.delete_pages")
def test_bulk_edit_delete_pages_rejects_pages_below_one(self, m) -> None:
"""
GIVEN:
- A legacy delete_pages bulk edit
WHEN:
- API to bulk edit is called with a page number below 1
THEN:
- API returns HTTP 400
- delete_pages is not called
"""
self.setup_mock(m, "delete_pages")
for pages in ([0], [-1], [1, 0]):
with self.subTest(pages=pages):
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc2.id],
"method": "delete_pages",
"parameters": {"pages": pages},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"pages must be positive integers", response.content)
m.assert_not_called()
@mock.patch("documents.views.bulk_edit.rotate") @mock.patch("documents.views.bulk_edit.rotate")
def test_rotate_insufficient_permissions(self, m) -> None: def test_rotate_insufficient_permissions(self, m) -> None:
self.doc1.owner = User.objects.get(username="temp_admin") self.doc1.owner = User.objects.get(username="temp_admin")
+152 -231
View File
@@ -1,9 +1,9 @@
import shutil import shutil
from collections.abc import Callable
from datetime import date from datetime import date
from pathlib import Path from pathlib import Path
from unittest import mock from unittest import mock
import pikepdf
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth.models import User from django.contrib.auth.models import User
@@ -21,6 +21,7 @@ from documents.models import Document
from documents.models import DocumentType from documents.models import DocumentType
from documents.models import StoragePath from documents.models import StoragePath
from documents.models import Tag from documents.models import Tag
from documents.pdf_ops import PageSpec
from documents.permissions import set_permissions_for_objects from documents.permissions import set_permissions_for_objects
from paperless_testing.dirs import DirectoriesMixin from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.permissions import grant_object from paperless_testing.permissions import grant_object
@@ -793,16 +794,14 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.img_doc.save() self.img_doc.save()
@staticmethod @staticmethod
def mock_password_required_pdf( def fake_decrypt(
mock_open: mock.Mock, src: Path,
fake_pdf: mock.Mock, make_dst: Callable[[], Path],
) -> None: password: str,
password_context = mock.MagicMock() ) -> Path:
password_context.__enter__.return_value = fake_pdf dst = make_dst()
mock_open.side_effect = [ dst.write_bytes(b"password removed")
pikepdf.PasswordError("password required"), return dst
password_context,
]
@mock.patch("documents.tasks.consume_file.s") @mock.patch("documents.tasks.consume_file.s")
def test_merge(self, mock_consume_file) -> None: def test_merge(self, mock_consume_file) -> None:
@@ -847,12 +846,12 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
@mock.patch("pikepdf.open") @mock.patch("documents.pdf_ops.PdfMerger")
@mock.patch("documents.tasks.consume_file.s") @mock.patch("documents.tasks.consume_file.s")
def test_merge_uses_latest_version_source_for_root_selection( def test_merge_uses_latest_version_source_for_root_selection(
self, self,
mock_consume_file, mock_consume_file,
mock_open_pdf, mock_merger,
) -> None: ) -> None:
version_file = self.dirs.scratch_dir / "sample2_version_merge.pdf" version_file = self.dirs.scratch_dir / "sample2_version_merge.pdf"
shutil.copy(self.doc2.source_path, version_file) shutil.copy(self.doc2.source_path, version_file)
@@ -863,16 +862,14 @@ class TestPDFActions(DirectoriesMixin, TestCase):
filename=version_file, filename=version_file,
mime_type="application/pdf", mime_type="application/pdf",
) )
fake_pdf = mock.MagicMock() merger = mock_merger.return_value.__enter__.return_value
fake_pdf.pdf_version = "1.7" merger.save.side_effect = lambda dst: shutil.copy(version.source_path, dst)
fake_pdf.pages = [mock.Mock()]
mock_open_pdf.return_value.__enter__.return_value = fake_pdf
result = bulk_edit.merge([self.doc2.id]) result = bulk_edit.merge([self.doc2.id])
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
mock_open_pdf.assert_called_once_with(str(version.source_path)) merger.add.assert_called_once_with(version.source_path)
mock_consume_file.assert_not_called() mock_consume_file.assert_called_once()
@mock.patch("documents.bulk_edit.delete.si") @mock.patch("documents.bulk_edit.delete.si")
@mock.patch("documents.tasks.consume_file.s") @mock.patch("documents.tasks.consume_file.s")
@@ -1034,18 +1031,18 @@ class TestPDFActions(DirectoriesMixin, TestCase):
) )
@mock.patch("documents.tasks.consume_file.delay") @mock.patch("documents.tasks.consume_file.delay")
@mock.patch("pikepdf.open") @mock.patch("documents.pdf_ops.PdfMerger.add")
def test_merge_with_errors(self, mock_open_pdf, mock_consume_file) -> None: def test_merge_with_errors(self, mock_add, mock_consume_file) -> None:
""" """
GIVEN: GIVEN:
- Existing documents - Existing documents
WHEN: WHEN:
- Merge action is called with 2 documents - Merge action is called with 2 documents
- Error occurs when opening both files - Error occurs when adding both files
THEN: THEN:
- Consume file should not be called - Consume file should not be called
""" """
mock_open_pdf.side_effect = Exception("Error opening PDF") mock_add.side_effect = Exception("Error opening PDF")
doc_ids = [self.doc2.id, self.doc3.id] doc_ids = [self.doc2.id, self.doc3.id]
with self.assertLogs("paperless.bulk_edit", level="ERROR") as cm: with self.assertLogs("paperless.bulk_edit", level="ERROR") as cm:
@@ -1082,12 +1079,12 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
@mock.patch("documents.bulk_edit.group") @mock.patch("documents.bulk_edit.group")
@mock.patch("pikepdf.open") @mock.patch("documents.pdf_ops.build_pdfs")
@mock.patch("documents.tasks.consume_file.s") @mock.patch("documents.tasks.consume_file.s")
def test_split_uses_latest_version_source_for_root_selection( def test_split_uses_latest_version_source_for_root_selection(
self, self,
mock_consume_file, mock_consume_file,
mock_open_pdf, mock_build_pdfs,
mock_group, mock_group,
) -> None: ) -> None:
version_file = self.dirs.scratch_dir / "sample2_version_split.pdf" version_file = self.dirs.scratch_dir / "sample2_version_split.pdf"
@@ -1099,17 +1096,15 @@ class TestPDFActions(DirectoriesMixin, TestCase):
filename=version_file, filename=version_file,
mime_type="application/pdf", mime_type="application/pdf",
) )
fake_pdf = mock.MagicMock() mock_build_pdfs.return_value = [version.source_path, version.source_path]
fake_pdf.pages = [mock.Mock(), mock.Mock()]
mock_open_pdf.return_value.__enter__.return_value = fake_pdf
mock_group.return_value.delay.return_value = None mock_group.return_value.delay.return_value = None
result = bulk_edit.split([self.doc2.id], [[1], [2]]) result = bulk_edit.split([self.doc2.id], [[1], [2]])
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
mock_open_pdf.assert_called_once_with(version.source_path) self.assertEqual(mock_build_pdfs.call_args.args[0], version.source_path)
mock_consume_file.assert_not_called() self.assertEqual(mock_consume_file.call_count, 2)
mock_group.return_value.delay.assert_not_called() mock_group.return_value.delay.assert_called_once()
@mock.patch("documents.bulk_edit.delete.si") @mock.patch("documents.bulk_edit.delete.si")
@mock.patch("documents.tasks.consume_file.s") @mock.patch("documents.tasks.consume_file.s")
@@ -1197,18 +1192,18 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.assertEqual(self.doc2.archive_serial_number, 222) self.assertEqual(self.doc2.archive_serial_number, 222)
@mock.patch("documents.tasks.consume_file.apply_async") @mock.patch("documents.tasks.consume_file.apply_async")
@mock.patch("pikepdf.Pdf.save") @mock.patch("documents.pdf_ops.build_pdfs")
def test_split_with_errors(self, mock_save_pdf, mock_consume_file) -> None: def test_split_with_errors(self, mock_build_pdfs, mock_consume_file) -> None:
""" """
GIVEN: GIVEN:
- Existing documents - Existing documents
WHEN: WHEN:
- Split action is called with 1 document and 2 page groups - Split action is called with 1 document and 2 page groups
- Error occurs when saving the files - Error occurs when building the files
THEN: THEN:
- Consume file should not be called - Consume file should not be called
""" """
mock_save_pdf.side_effect = Exception("Error saving PDF") mock_build_pdfs.side_effect = Exception("Error building PDFs")
doc_ids = [self.doc2.id] doc_ids = [self.doc2.id]
pages = [[1, 2], [3]] pages = [[1, 2], [3]]
@@ -1243,10 +1238,10 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
@mock.patch("documents.tasks.consume_file.apply_async") @mock.patch("documents.tasks.consume_file.apply_async")
@mock.patch("pikepdf.Pdf.save") @mock.patch("documents.pdf_ops.rotate_pdf")
def test_rotate_with_error( def test_rotate_with_error(
self, self,
mock_pdf_save, mock_rotate_pdf,
mock_consume_delay, mock_consume_delay,
) -> None: ) -> None:
""" """
@@ -1254,11 +1249,11 @@ class TestPDFActions(DirectoriesMixin, TestCase):
- Existing documents - Existing documents
WHEN: WHEN:
- Rotate action is called with 2 documents - Rotate action is called with 2 documents
- PikePDF raises an error - Rotating the PDF raises an error
THEN: THEN:
- Rotate action should be called 0 times - Rotate action should be called 0 times
""" """
mock_pdf_save.side_effect = Exception("Error saving PDF") mock_rotate_pdf.side_effect = Exception("Error rotating PDF")
doc_ids = [self.doc2.id, self.doc3.id] doc_ids = [self.doc2.id, self.doc3.id]
with self.assertLogs("paperless.bulk_edit", level="ERROR") as cm: with self.assertLogs("paperless.bulk_edit", level="ERROR") as cm:
@@ -1293,10 +1288,10 @@ class TestPDFActions(DirectoriesMixin, TestCase):
@mock.patch("documents.data_models.magic.from_file", return_value="application/pdf") @mock.patch("documents.data_models.magic.from_file", return_value="application/pdf")
@mock.patch("documents.tasks.consume_file.apply_async") @mock.patch("documents.tasks.consume_file.apply_async")
@mock.patch("pikepdf.open") @mock.patch("documents.pdf_ops.rotate_pdf")
def test_rotate_explicit_selection_uses_root_source_when_root_selected( def test_rotate_explicit_selection_uses_root_source_when_root_selected(
self, self,
mock_open, mock_rotate_pdf,
mock_consume_delay, mock_consume_delay,
mock_magic, mock_magic,
) -> None: ) -> None:
@@ -1305,9 +1300,6 @@ class TestPDFActions(DirectoriesMixin, TestCase):
title="B version 1", title="B version 1",
root_document=self.doc2, root_document=self.doc2,
) )
fake_pdf = mock.MagicMock()
fake_pdf.pages = [mock.Mock()]
mock_open.return_value.__enter__.return_value = fake_pdf
result = bulk_edit.rotate( result = bulk_edit.rotate(
[self.doc2.id], [self.doc2.id],
@@ -1316,26 +1308,35 @@ class TestPDFActions(DirectoriesMixin, TestCase):
) )
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
mock_open.assert_called_once_with(self.doc2.source_path) self.assertEqual(mock_rotate_pdf.call_args.args[0], self.doc2.source_path)
mock_consume_delay.assert_called_once() mock_consume_delay.assert_called_once()
@mock.patch("documents.tasks.consume_file.apply_async") @mock.patch("documents.tasks.consume_file.apply_async")
@mock.patch("pikepdf.Pdf.save") @mock.patch("documents.pdf_ops.remove_pages")
@mock.patch("documents.data_models.magic.from_file", return_value="application/pdf") @mock.patch("documents.data_models.magic.from_file", return_value="application/pdf")
def test_delete_pages(self, mock_magic, mock_pdf_save, mock_consume_delay) -> None: def test_delete_pages(
self,
mock_magic,
mock_remove_pages,
mock_consume_delay,
) -> None:
""" """
GIVEN: GIVEN:
- Existing documents - Existing documents
WHEN: WHEN:
- Delete pages action is called with 1 document and 2 pages - Delete pages action is called with 1 document and 2 pages
THEN: THEN:
- Save should be called once - The pages are removed from the document's source PDF
- A new version should be enqueued via consume_file - A new version should be enqueued via consume_file
""" """
doc_ids = [self.doc2.id] doc_ids = [self.doc2.id]
pages = [1, 3] pages = [1, 3]
result = bulk_edit.delete_pages(doc_ids, pages) result = bulk_edit.delete_pages(doc_ids, pages)
mock_pdf_save.assert_called_once() mock_remove_pages.assert_called_once_with(
self.doc2.source_path,
mock.ANY,
[1, 3],
)
mock_consume_delay.assert_called_once() mock_consume_delay.assert_called_once()
task_kwargs = mock_consume_delay.call_args.kwargs["kwargs"] task_kwargs = mock_consume_delay.call_args.kwargs["kwargs"]
self.assertEqual(task_kwargs["input_doc"].root_document_id, self.doc2.id) self.assertEqual(task_kwargs["input_doc"].root_document_id, self.doc2.id)
@@ -1347,10 +1348,10 @@ class TestPDFActions(DirectoriesMixin, TestCase):
@mock.patch("documents.data_models.magic.from_file", return_value="application/pdf") @mock.patch("documents.data_models.magic.from_file", return_value="application/pdf")
@mock.patch("documents.tasks.consume_file.apply_async") @mock.patch("documents.tasks.consume_file.apply_async")
@mock.patch("pikepdf.open") @mock.patch("documents.pdf_ops.remove_pages")
def test_delete_pages_explicit_selection_uses_root_source_when_root_selected( def test_delete_pages_explicit_selection_uses_root_source_when_root_selected(
self, self,
mock_open, mock_remove_pages,
mock_consume_delay, mock_consume_delay,
mock_magic, mock_magic,
) -> None: ) -> None:
@@ -1359,9 +1360,6 @@ class TestPDFActions(DirectoriesMixin, TestCase):
title="B version 1", title="B version 1",
root_document=self.doc2, root_document=self.doc2,
) )
fake_pdf = mock.MagicMock()
fake_pdf.pages = [mock.Mock(), mock.Mock()]
mock_open.return_value.__enter__.return_value = fake_pdf
result = bulk_edit.delete_pages( result = bulk_edit.delete_pages(
[self.doc2.id], [self.doc2.id],
@@ -1370,23 +1368,26 @@ class TestPDFActions(DirectoriesMixin, TestCase):
) )
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
mock_open.assert_called_once_with(self.doc2.source_path) self.assertEqual(mock_remove_pages.call_args.args[0], self.doc2.source_path)
mock_consume_delay.assert_called_once() mock_consume_delay.assert_called_once()
@mock.patch("documents.tasks.consume_file.apply_async") @mock.patch("documents.tasks.consume_file.apply_async")
@mock.patch("pikepdf.Pdf.save") @mock.patch("documents.pdf_ops.remove_pages")
def test_delete_pages_with_error(self, mock_pdf_save, mock_consume_delay) -> None: def test_delete_pages_with_error(
self,
mock_remove_pages,
mock_consume_delay,
) -> None:
""" """
GIVEN: GIVEN:
- Existing documents - Existing documents
WHEN: WHEN:
- Delete pages action is called with 1 document and 2 pages - Delete pages action is called with 1 document and 2 pages
- PikePDF raises an error - Removing the pages raises an error
THEN: THEN:
- Save should be called once
- No new version should be enqueued - No new version should be enqueued
""" """
mock_pdf_save.side_effect = Exception("Error saving PDF") mock_remove_pages.side_effect = Exception("Error removing pages")
doc_ids = [self.doc2.id] doc_ids = [self.doc2.id]
pages = [1, 3] pages = [1, 3]
@@ -1416,6 +1417,41 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
mock_group.return_value.delay.assert_called_once() mock_group.return_value.delay.assert_called_once()
@mock.patch("documents.bulk_edit.group")
@mock.patch("documents.pdf_ops.build_pdfs")
@mock.patch("documents.tasks.consume_file.s")
def test_edit_pdf_maps_operations_to_outputs(
self,
mock_consume_file: mock.Mock,
mock_build_pdfs: mock.Mock,
mock_group: mock.Mock,
) -> None:
"""
GIVEN:
- Existing document
WHEN:
- edit_pdf is called with operations interleaved across two outputs,
some of them rotated
THEN:
- Each output is built from its own operations, in operation order,
with the requested rotation
"""
mock_build_pdfs.return_value = [self.doc2.source_path, self.doc2.source_path]
mock_group.return_value.delay.return_value = None
operations = [
{"page": 3, "doc": 1},
{"page": 1, "doc": 0, "rotate": 90},
{"page": 2, "doc": 1, "rotate": 180},
]
bulk_edit.edit_pdf([self.doc2.id], operations)
outputs = mock_build_pdfs.call_args.args[1]
self.assertEqual(
[specs for specs, _ in outputs],
[[PageSpec(1, 90)], [PageSpec(3), PageSpec(2, 180)]],
)
@mock.patch("documents.bulk_edit.group") @mock.patch("documents.bulk_edit.group")
@mock.patch("documents.tasks.consume_file.s") @mock.patch("documents.tasks.consume_file.s")
def test_edit_pdf_with_user_override(self, mock_consume_file, mock_group) -> None: def test_edit_pdf_with_user_override(self, mock_consume_file, mock_group) -> None:
@@ -1531,12 +1567,10 @@ class TestPDFActions(DirectoriesMixin, TestCase):
@mock.patch("documents.data_models.magic.from_file", return_value="application/pdf") @mock.patch("documents.data_models.magic.from_file", return_value="application/pdf")
@mock.patch("documents.tasks.consume_file.apply_async") @mock.patch("documents.tasks.consume_file.apply_async")
@mock.patch("pikepdf.new") @mock.patch("documents.pdf_ops.build_pdfs")
@mock.patch("pikepdf.open")
def test_edit_pdf_explicit_selection_uses_root_source_when_root_selected( def test_edit_pdf_explicit_selection_uses_root_source_when_root_selected(
self, self,
mock_open, mock_build_pdfs,
mock_new,
mock_consume_delay, mock_consume_delay,
mock_magic, mock_magic,
) -> None: ) -> None:
@@ -1545,12 +1579,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
title="B version 1", title="B version 1",
root_document=self.doc2, root_document=self.doc2,
) )
fake_pdf = mock.MagicMock() mock_build_pdfs.return_value = [Path("edited.pdf")]
fake_pdf.pages = [mock.Mock()]
mock_open.return_value.__enter__.return_value = fake_pdf
output_pdf = mock.MagicMock()
output_pdf.pages = []
mock_new.return_value = output_pdf
result = bulk_edit.edit_pdf( result = bulk_edit.edit_pdf(
[self.doc2.id], [self.doc2.id],
@@ -1560,7 +1589,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
) )
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
mock_open.assert_called_once_with(self.doc2.source_path) self.assertEqual(mock_build_pdfs.call_args.args[0], self.doc2.source_path)
mock_consume_delay.assert_called_once() mock_consume_delay.assert_called_once()
@mock.patch("documents.bulk_edit.group") @mock.patch("documents.bulk_edit.group")
@@ -1586,31 +1615,6 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
mock_group.return_value.delay.assert_called_once() mock_group.return_value.delay.assert_called_once()
@mock.patch("documents.bulk_edit.group")
@mock.patch("documents.tasks.consume_file.s")
def test_edit_pdf_open_failure(
self,
mock_consume_file: mock.Mock,
mock_group: mock.Mock,
) -> None:
"""
GIVEN:
- Existing document
WHEN:
- edit_pdf fails to open PDF
THEN:
- Task group is not called
"""
doc_ids = [self.doc2.id]
operations = [
{"page": 9999}, # invalid page, forces error during PDF load
]
with self.assertLogs("paperless.bulk_edit", level="ERROR"):
with self.assertRaises(Exception):
bulk_edit.edit_pdf(doc_ids, operations)
mock_group.assert_not_called()
mock_consume_file.assert_not_called()
@mock.patch("documents.bulk_edit.group") @mock.patch("documents.bulk_edit.group")
@mock.patch("documents.tasks.consume_file.s") @mock.patch("documents.tasks.consume_file.s")
def test_edit_pdf_multiple_outputs_with_update_flag_errors( def test_edit_pdf_multiple_outputs_with_update_flag_errors(
@@ -1637,23 +1641,15 @@ class TestPDFActions(DirectoriesMixin, TestCase):
mock_group.assert_not_called() mock_group.assert_not_called()
mock_consume_file.assert_not_called() mock_consume_file.assert_not_called()
@mock.patch("pikepdf.open")
def test_edit_pdf_rejects_invalid_operations(self, mock_open) -> None:
for operations in ([], [{"page": 1, "doc": 2**32}]):
with self.subTest(operations=operations):
with self.assertLogs("paperless.bulk_edit", level="ERROR"):
with self.assertRaisesRegex(ValueError, "index is out of bounds"):
bulk_edit.edit_pdf([self.doc2.id], operations)
mock_open.assert_not_called()
@mock.patch("documents.bulk_edit.update_document_content_maybe_archive_file.delay") @mock.patch("documents.bulk_edit.update_document_content_maybe_archive_file.delay")
@mock.patch("documents.tasks.consume_file.apply_async") @mock.patch("documents.tasks.consume_file.apply_async")
@mock.patch("documents.bulk_edit.tempfile.mkdtemp") @mock.patch("documents.bulk_edit.tempfile.mkdtemp")
@mock.patch("pikepdf.open") @mock.patch("documents.pdf_ops.decrypt_pdf")
@mock.patch("documents.pdf_ops.needs_decrypt", return_value=True)
def test_remove_password_update_document( def test_remove_password_update_document(
self, self,
mock_open, mock_needs_decrypt,
mock_decrypt,
mock_mkdtemp, mock_mkdtemp,
mock_consume_delay, mock_consume_delay,
mock_update_document, mock_update_document,
@@ -1662,16 +1658,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
temp_dir = self.dirs.scratch_dir / "remove-password-update" temp_dir = self.dirs.scratch_dir / "remove-password-update"
temp_dir.mkdir(parents=True, exist_ok=True) temp_dir.mkdir(parents=True, exist_ok=True)
mock_mkdtemp.return_value = str(temp_dir) mock_mkdtemp.return_value = str(temp_dir)
mock_decrypt.side_effect = self.fake_decrypt
fake_pdf = mock.MagicMock()
fake_pdf.pages = [mock.Mock(), mock.Mock(), mock.Mock()]
fake_pdf.is_encrypted = True
def save_side_effect(target_path):
Path(target_path).write_bytes(b"new pdf content")
fake_pdf.save.side_effect = save_side_effect
mock_open.return_value.__enter__.return_value = fake_pdf
result = bulk_edit.remove_password( result = bulk_edit.remove_password(
[doc.id], [doc.id],
@@ -1680,14 +1667,8 @@ class TestPDFActions(DirectoriesMixin, TestCase):
) )
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
self.assertEqual( mock_needs_decrypt.assert_called_once_with(doc.source_path)
mock_open.call_args_list, mock_decrypt.assert_called_once_with(doc.source_path, mock.ANY, "secret")
[
mock.call(doc.source_path),
mock.call(doc.source_path, password="secret"),
],
)
fake_pdf.remove_unreferenced_resources.assert_called_once()
mock_update_document.assert_not_called() mock_update_document.assert_not_called()
mock_consume_delay.assert_called_once() mock_consume_delay.assert_called_once()
task_kwargs = mock_consume_delay.call_args.kwargs["kwargs"] task_kwargs = mock_consume_delay.call_args.kwargs["kwargs"]
@@ -1700,40 +1681,15 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.assertEqual(task_kwargs["input_doc"].root_document_id, doc.id) self.assertEqual(task_kwargs["input_doc"].root_document_id, doc.id)
self.assertIsNotNone(task_kwargs["overrides"]) self.assertIsNotNone(task_kwargs["overrides"])
@mock.patch("documents.tasks.consume_file.apply_async")
@mock.patch("documents.bulk_edit.tempfile.mkdtemp")
@mock.patch("pikepdf.open")
def test_remove_password_update_document_skips_unencrypted_pdf(
self,
mock_open,
mock_mkdtemp,
mock_consume_delay,
) -> None:
doc = self.doc1
fake_pdf = mock.MagicMock()
fake_pdf.is_encrypted = False
mock_open.return_value.__enter__.return_value = fake_pdf
result = bulk_edit.remove_password(
[doc.id],
password="secret",
update_document=True,
)
self.assertEqual(result, "OK")
mock_open.assert_called_once_with(doc.source_path)
fake_pdf.remove_unreferenced_resources.assert_not_called()
fake_pdf.save.assert_not_called()
mock_mkdtemp.assert_not_called()
mock_consume_delay.assert_not_called()
@mock.patch("documents.bulk_edit.update_document_content_maybe_archive_file.delay") @mock.patch("documents.bulk_edit.update_document_content_maybe_archive_file.delay")
@mock.patch("documents.tasks.consume_file.apply_async") @mock.patch("documents.tasks.consume_file.apply_async")
@mock.patch("documents.bulk_edit.tempfile.mkdtemp") @mock.patch("documents.bulk_edit.tempfile.mkdtemp")
@mock.patch("pikepdf.open") @mock.patch("documents.pdf_ops.decrypt_pdf")
@mock.patch("documents.pdf_ops.needs_decrypt", return_value=True)
def test_remove_password_update_document_uses_source_paths( def test_remove_password_update_document_uses_source_paths(
self, self,
mock_open, mock_needs_decrypt,
mock_decrypt,
mock_mkdtemp, mock_mkdtemp,
mock_consume_delay, mock_consume_delay,
mock_update_document, mock_update_document,
@@ -1744,14 +1700,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
temp_dir = self.dirs.scratch_dir / "remove-password-source-file" temp_dir = self.dirs.scratch_dir / "remove-password-source-file"
temp_dir.mkdir(parents=True, exist_ok=True) temp_dir.mkdir(parents=True, exist_ok=True)
mock_mkdtemp.return_value = str(temp_dir) mock_mkdtemp.return_value = str(temp_dir)
mock_decrypt.side_effect = self.fake_decrypt
fake_pdf = mock.MagicMock()
self.mock_password_required_pdf(mock_open, fake_pdf)
def save_side_effect(target_path):
Path(target_path).write_bytes(b"new pdf content")
fake_pdf.save.side_effect = save_side_effect
result = bulk_edit.remove_password( result = bulk_edit.remove_password(
[doc.id], [doc.id],
@@ -1761,22 +1710,19 @@ class TestPDFActions(DirectoriesMixin, TestCase):
) )
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
self.assertEqual( mock_needs_decrypt.assert_called_once_with(source_file)
mock_open.call_args_list, mock_decrypt.assert_called_once_with(source_file, mock.ANY, "secret")
[
mock.call(source_file),
mock.call(source_file, password="secret"),
],
)
mock_update_document.assert_not_called() mock_update_document.assert_not_called()
mock_consume_delay.assert_called_once() mock_consume_delay.assert_called_once()
@mock.patch("documents.data_models.magic.from_file", return_value="application/pdf") @mock.patch("documents.data_models.magic.from_file", return_value="application/pdf")
@mock.patch("documents.tasks.consume_file.apply_async") @mock.patch("documents.tasks.consume_file.apply_async")
@mock.patch("pikepdf.open") @mock.patch("documents.pdf_ops.decrypt_pdf")
@mock.patch("documents.pdf_ops.needs_decrypt", return_value=True)
def test_remove_password_explicit_selection_uses_root_source_when_root_selected( def test_remove_password_explicit_selection_uses_root_source_when_root_selected(
self, self,
mock_open, mock_needs_decrypt,
mock_decrypt,
mock_consume_delay, mock_consume_delay,
mock_magic, mock_magic,
) -> None: ) -> None:
@@ -1785,8 +1731,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
title="A version 1", title="A version 1",
root_document=self.doc1, root_document=self.doc1,
) )
fake_pdf = mock.MagicMock() mock_decrypt.return_value = Path("unprotected.pdf")
self.mock_password_required_pdf(mock_open, fake_pdf)
result = bulk_edit.remove_password( result = bulk_edit.remove_password(
[self.doc1.id], [self.doc1.id],
@@ -1796,12 +1741,11 @@ class TestPDFActions(DirectoriesMixin, TestCase):
) )
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
self.assertEqual( mock_needs_decrypt.assert_called_once_with(self.doc1.source_path)
mock_open.call_args_list, mock_decrypt.assert_called_once_with(
[ self.doc1.source_path,
mock.call(self.doc1.source_path), mock.ANY,
mock.call(self.doc1.source_path, password="secret"), "secret",
],
) )
mock_consume_delay.assert_called_once() mock_consume_delay.assert_called_once()
@@ -1809,10 +1753,12 @@ class TestPDFActions(DirectoriesMixin, TestCase):
@mock.patch("documents.bulk_edit.group") @mock.patch("documents.bulk_edit.group")
@mock.patch("documents.tasks.consume_file.s") @mock.patch("documents.tasks.consume_file.s")
@mock.patch("documents.bulk_edit.tempfile.mkdtemp") @mock.patch("documents.bulk_edit.tempfile.mkdtemp")
@mock.patch("pikepdf.open") @mock.patch("documents.pdf_ops.decrypt_pdf")
@mock.patch("documents.pdf_ops.needs_decrypt", return_value=True)
def test_remove_password_creates_consumable_document( def test_remove_password_creates_consumable_document(
self, self,
mock_open: mock.Mock, mock_needs_decrypt: mock.Mock,
mock_decrypt: mock.Mock,
mock_mkdtemp: mock.Mock, mock_mkdtemp: mock.Mock,
mock_consume_file: mock.Mock, mock_consume_file: mock.Mock,
mock_group: mock.Mock, mock_group: mock.Mock,
@@ -1822,15 +1768,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
temp_dir = self.dirs.scratch_dir / "remove-password" temp_dir = self.dirs.scratch_dir / "remove-password"
temp_dir.mkdir(parents=True, exist_ok=True) temp_dir.mkdir(parents=True, exist_ok=True)
mock_mkdtemp.return_value = str(temp_dir) mock_mkdtemp.return_value = str(temp_dir)
mock_decrypt.side_effect = self.fake_decrypt
fake_pdf = mock.MagicMock()
fake_pdf.pages = [mock.Mock(), mock.Mock()]
self.mock_password_required_pdf(mock_open, fake_pdf)
def save_side_effect(target_path: Path) -> None:
target_path.write_bytes(b"password removed")
fake_pdf.save.side_effect = save_side_effect
mock_group.return_value.delay.return_value = None mock_group.return_value.delay.return_value = None
user = User.objects.create(username="owner") user = User.objects.create(username="owner")
@@ -1845,13 +1783,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
) )
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
self.assertEqual( mock_decrypt.assert_called_once_with(doc.source_path, mock.ANY, "secret")
mock_open.call_args_list,
[
mock.call(doc.source_path),
mock.call(doc.source_path, password="secret"),
],
)
mock_consume_file.assert_called_once() mock_consume_file.assert_called_once()
call_kwargs = mock_consume_file.call_args.kwargs call_kwargs = mock_consume_file.call_args.kwargs
consumable_document = call_kwargs["input_doc"] consumable_document = call_kwargs["input_doc"]
@@ -1873,21 +1805,18 @@ class TestPDFActions(DirectoriesMixin, TestCase):
@mock.patch("documents.bulk_edit.chord") @mock.patch("documents.bulk_edit.chord")
@mock.patch("documents.bulk_edit.group") @mock.patch("documents.bulk_edit.group")
@mock.patch("documents.tasks.consume_file.s") @mock.patch("documents.tasks.consume_file.s")
@mock.patch("documents.bulk_edit.tempfile.mkdtemp") @mock.patch("documents.pdf_ops.decrypt_pdf")
@mock.patch("pikepdf.open") @mock.patch("documents.pdf_ops.needs_decrypt", return_value=False)
def test_remove_password_skips_unencrypted_pdf_without_queueing( def test_remove_password_skips_unencrypted_pdf_without_queueing(
self, self,
mock_open: mock.Mock, mock_needs_decrypt: mock.Mock,
mock_mkdtemp: mock.Mock, mock_decrypt: mock.Mock,
mock_consume_file: mock.Mock, mock_consume_file: mock.Mock,
mock_group: mock.Mock, mock_group: mock.Mock,
mock_chord: mock.Mock, mock_chord: mock.Mock,
mock_delete: mock.Mock, mock_delete: mock.Mock,
) -> None: ) -> None:
doc = self.doc2 doc = self.doc2
fake_pdf = mock.MagicMock()
fake_pdf.is_encrypted = False
mock_open.return_value.__enter__.return_value = fake_pdf
result = bulk_edit.remove_password( result = bulk_edit.remove_password(
[doc.id], [doc.id],
@@ -1897,10 +1826,8 @@ class TestPDFActions(DirectoriesMixin, TestCase):
) )
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
mock_open.assert_called_once_with(doc.source_path) mock_needs_decrypt.assert_called_once_with(doc.source_path)
fake_pdf.remove_unreferenced_resources.assert_not_called() mock_decrypt.assert_not_called()
fake_pdf.save.assert_not_called()
mock_mkdtemp.assert_not_called()
mock_consume_file.assert_not_called() mock_consume_file.assert_not_called()
mock_group.assert_not_called() mock_group.assert_not_called()
mock_chord.assert_not_called() mock_chord.assert_not_called()
@@ -1911,10 +1838,12 @@ class TestPDFActions(DirectoriesMixin, TestCase):
@mock.patch("documents.bulk_edit.group") @mock.patch("documents.bulk_edit.group")
@mock.patch("documents.tasks.consume_file.s") @mock.patch("documents.tasks.consume_file.s")
@mock.patch("documents.bulk_edit.tempfile.mkdtemp") @mock.patch("documents.bulk_edit.tempfile.mkdtemp")
@mock.patch("pikepdf.open") @mock.patch("documents.pdf_ops.decrypt_pdf")
@mock.patch("documents.pdf_ops.needs_decrypt", return_value=True)
def test_remove_password_deletes_original( def test_remove_password_deletes_original(
self, self,
mock_open: mock.Mock, mock_needs_decrypt: mock.Mock,
mock_decrypt: mock.Mock,
mock_mkdtemp: mock.Mock, mock_mkdtemp: mock.Mock,
mock_consume_file: mock.Mock, mock_consume_file: mock.Mock,
mock_group: mock.Mock, mock_group: mock.Mock,
@@ -1925,15 +1854,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
temp_dir = self.dirs.scratch_dir / "remove-password-delete" temp_dir = self.dirs.scratch_dir / "remove-password-delete"
temp_dir.mkdir(parents=True, exist_ok=True) temp_dir.mkdir(parents=True, exist_ok=True)
mock_mkdtemp.return_value = str(temp_dir) mock_mkdtemp.return_value = str(temp_dir)
mock_decrypt.side_effect = self.fake_decrypt
fake_pdf = mock.MagicMock()
fake_pdf.pages = [mock.Mock(), mock.Mock()]
self.mock_password_required_pdf(mock_open, fake_pdf)
def save_side_effect(target_path: Path) -> None:
target_path.write_bytes(b"password removed")
fake_pdf.save.side_effect = save_side_effect
mock_chord.return_value.delay.return_value = None mock_chord.return_value.delay.return_value = None
result = bulk_edit.remove_password( result = bulk_edit.remove_password(
@@ -1945,23 +1866,23 @@ class TestPDFActions(DirectoriesMixin, TestCase):
) )
self.assertEqual(result, "OK") self.assertEqual(result, "OK")
self.assertEqual( mock_decrypt.assert_called_once_with(doc.source_path, mock.ANY, "secret")
mock_open.call_args_list,
[
mock.call(doc.source_path),
mock.call(doc.source_path, password="secret"),
],
)
mock_consume_file.assert_called_once() mock_consume_file.assert_called_once()
mock_group.assert_not_called() mock_group.assert_not_called()
mock_chord.assert_called_once() mock_chord.assert_called_once()
mock_chord.return_value.delay.assert_called_once() mock_chord.return_value.delay.assert_called_once()
mock_delete.si.assert_called_once_with([doc.id]) mock_delete.si.assert_called_once_with([doc.id])
@mock.patch("pikepdf.open") @mock.patch(
def test_remove_password_open_failure(self, mock_open: mock.Mock) -> None: "documents.pdf_ops.decrypt_pdf",
mock_open.side_effect = RuntimeError("wrong password") side_effect=RuntimeError("wrong password"),
)
@mock.patch("documents.pdf_ops.needs_decrypt", return_value=True)
def test_remove_password_failure_raises_value_error(
self,
mock_needs_decrypt: mock.Mock,
mock_decrypt: mock.Mock,
) -> None:
with self.assertLogs("paperless.bulk_edit", level="ERROR") as cm: with self.assertLogs("paperless.bulk_edit", level="ERROR") as cm:
with self.assertRaises(ValueError) as exc: with self.assertRaises(ValueError) as exc:
bulk_edit.remove_password([self.doc1.id], password="secret") bulk_edit.remove_password([self.doc1.id], password="secret")
+520
View File
@@ -0,0 +1,520 @@
"""
Tests for documents.pdf_ops.
These use real PDFs from the sample directories. No database, Celery or mocks.
Pages are compared by a hash of their content stream, so page identity and order
are easy to assert.
"""
import hashlib
from collections.abc import Callable
from pathlib import Path
import pikepdf
import pytest
from documents import pdf_ops
from documents.pdf_ops import PageSpec
SRC_ROOT = Path(__file__).parents[2]
SAMPLES = Path(__file__).parent / "samples"
THREE_PAGES = SAMPLES / "documents" / "originals" / "0000002.pdf"
TWELVE_PAGES = SAMPLES / "barcodes" / "split-by-asn-2.pdf"
ENCRYPTED = SAMPLES / "password-is-test.pdf"
SIGNED = SRC_ROOT / "paperless" / "tests" / "samples" / "tesseract" / "signed.pdf"
def _page_fingerprint(page: pikepdf.Page) -> str:
contents = page.obj.get("/Contents")
assert contents is not None, "sample page has no /Contents"
streams = list(contents) if isinstance(contents, pikepdf.Array) else [contents]
return hashlib.sha256(b"".join(s.read_bytes() for s in streams)).hexdigest()
def fingerprints(path: Path) -> list[str]:
with pikepdf.open(path) as pdf:
return [_page_fingerprint(page) for page in pdf.pages]
def rotations(path: Path) -> list[int]:
with pikepdf.open(path) as pdf:
return [int(page.obj.get("/Rotate", 0)) for page in pdf.pages]
def docinfo_keys(path: Path) -> set[str]:
with pikepdf.open(path) as pdf:
return set(pdf.docinfo.keys())
def constant(path: Path) -> Callable[[], Path]:
return lambda: path
@pytest.fixture
def source_fingerprints() -> list[str]:
fps = fingerprints(THREE_PAGES)
assert len(set(fps)) == 3, "sample must have three distinct pages"
return fps
class TestRotatePdf:
def test_rotation_is_relative_and_applies_to_every_page(
self,
tmp_path: Path,
) -> None:
"""
GIVEN:
- A three page PDF
WHEN:
- It is rotated by 90 degrees, then the result is rotated by 90 again
THEN:
- Every page is rotated relative to its current rotation
- The page content itself is unchanged
"""
once = tmp_path / "once.pdf"
twice = tmp_path / "twice.pdf"
pdf_ops.rotate_pdf(THREE_PAGES, once, 90)
pdf_ops.rotate_pdf(once, twice, 90)
assert rotations(once) == [90, 90, 90]
assert rotations(twice) == [180, 180, 180]
assert fingerprints(twice) == fingerprints(THREE_PAGES)
def test_keeps_document_info(self, tmp_path: Path) -> None:
"""
GIVEN:
- A PDF with document info
WHEN:
- It is rotated
THEN:
- The document info is still present in the output
"""
dst = tmp_path / "out.pdf"
pdf_ops.rotate_pdf(THREE_PAGES, dst, 90)
assert "/Creator" in docinfo_keys(dst)
class TestRemovePages:
@pytest.mark.parametrize(
("pages", "kept"),
[
pytest.param([2], [0, 2], id="single"),
pytest.param([3, 1], [1], id="unordered"),
pytest.param([2, 2], [0, 2], id="duplicates-remove-once"),
pytest.param([], [0, 1, 2], id="empty-keeps-everything"),
pytest.param([1, 2, 3], [], id="every-page"),
],
)
def test_removes_only_the_requested_pages(
self,
tmp_path: Path,
source_fingerprints: list[str],
pages: list[int],
kept: list[int],
) -> None:
"""
GIVEN:
- A three page PDF
WHEN:
- Pages are removed, in any order and possibly repeated
THEN:
- Exactly the other pages remain, in their original order
"""
dst = tmp_path / "out.pdf"
pdf_ops.remove_pages(THREE_PAGES, dst, pages)
assert fingerprints(dst) == [source_fingerprints[i] for i in kept]
def test_keeps_document_info(self, tmp_path: Path) -> None:
"""
GIVEN:
- A PDF with document info
WHEN:
- A page is removed
THEN:
- The document info is still present in the output
"""
dst = tmp_path / "out.pdf"
pdf_ops.remove_pages(THREE_PAGES, dst, [1])
assert "/Creator" in docinfo_keys(dst)
@pytest.mark.parametrize(
"bad_page",
[
pytest.param(0, id="zero"),
pytest.param(-1, id="negative"),
],
)
def test_rejects_pages_below_one(self, tmp_path: Path, bad_page: int) -> None:
"""
GIVEN:
- A three page PDF
WHEN:
- Pages are removed and one of them is below 1
THEN:
- ValueError is raised
- No output file is written
"""
dst = tmp_path / "out.pdf"
with pytest.raises(ValueError, match="start at 1"):
pdf_ops.remove_pages(THREE_PAGES, dst, [1, bad_page])
assert not dst.exists()
class TestBuildPdfs:
def test_selects_and_orders_pages(
self,
tmp_path: Path,
source_fingerprints: list[str],
) -> None:
"""
GIVEN:
- A three page PDF
WHEN:
- One output is built from pages 3 then 1
THEN:
- The output has those pages in that order
- Its path is returned
"""
dst = tmp_path / "out.pdf"
written = pdf_ops.build_pdfs(
THREE_PAGES,
[([PageSpec(3), PageSpec(1)], constant(dst))],
)
assert written == [dst]
assert fingerprints(dst) == [source_fingerprints[2], source_fingerprints[0]]
def test_rotates_only_the_requested_pages(self, tmp_path: Path) -> None:
"""
GIVEN:
- A three page PDF
WHEN:
- One output is built with a different rotation on each page
THEN:
- Each output page has exactly the rotation requested for it
"""
dst = tmp_path / "out.pdf"
pdf_ops.build_pdfs(
THREE_PAGES,
[([PageSpec(1), PageSpec(2, 90), PageSpec(3, 180)], constant(dst))],
)
assert rotations(dst) == [0, 90, 180]
def test_writes_one_file_per_output_in_order(self, tmp_path: Path) -> None:
"""
GIVEN:
- A twelve page PDF
WHEN:
- Two outputs are built from different page ranges
THEN:
- Two files are written and returned in output order
- Each holds exactly its own pages
"""
first = tmp_path / "first.pdf"
second = tmp_path / "second.pdf"
source = fingerprints(TWELVE_PAGES)
written = pdf_ops.build_pdfs(
TWELVE_PAGES,
[
([PageSpec(p) for p in (1, 2, 3)], constant(first)),
([PageSpec(p) for p in range(4, 13)], constant(second)),
],
)
assert written == [first, second]
assert fingerprints(first) == source[:3]
assert fingerprints(second) == source[3:]
def test_empty_page_list_writes_a_zero_page_file(self, tmp_path: Path) -> None:
"""
GIVEN:
- A three page PDF
WHEN:
- An output with no pages is built
THEN:
- A PDF with zero pages is written
"""
dst = tmp_path / "out.pdf"
pdf_ops.build_pdfs(THREE_PAGES, [([], constant(dst))])
assert fingerprints(dst) == []
def test_destination_is_not_requested_when_a_page_is_out_of_range(
self,
tmp_path: Path,
) -> None:
"""
GIVEN:
- A three page PDF
WHEN:
- Several outputs are built
- A later output refers to a page past the end
THEN:
- IndexError is raised
- No destination was requested for any output, including earlier valid ones
"""
requested: list[Path] = []
def make_dst() -> Path:
requested.append(tmp_path / "out.pdf")
return requested[-1]
with pytest.raises(IndexError):
pdf_ops.build_pdfs(
THREE_PAGES,
[([PageSpec(1)], make_dst), ([PageSpec(99)], make_dst)],
)
assert requested == []
@pytest.mark.parametrize(
"bad_page",
[
pytest.param(0, id="zero"),
pytest.param(-1, id="negative"),
],
)
def test_rejects_pages_below_one_before_opening_anything(
self,
tmp_path: Path,
bad_page: int,
) -> None:
"""
GIVEN:
- A three page PDF
WHEN:
- Several outputs are built and a later one has a page below 1
THEN:
- ValueError is raised
- No destination was requested for any output
"""
requested: list[Path] = []
def make_dst() -> Path:
requested.append(tmp_path / "out.pdf")
return requested[-1]
with pytest.raises(ValueError, match="start at 1"):
pdf_ops.build_pdfs(
THREE_PAGES,
[([PageSpec(1)], make_dst), ([PageSpec(bad_page)], make_dst)],
)
assert requested == []
class TestValidatePageOperations:
def test_returns_the_output_count(self) -> None:
"""
GIVEN:
- Operations that all target the default output
WHEN:
- They are validated
THEN:
- One output document is reported
"""
operations = [{"page": 1}, {"page": 2}, {"page": 3}]
assert pdf_ops.validate_page_operations(operations, single_output=True) == 1
def test_gap_in_output_indices_counts_up_to_the_highest(self) -> None:
"""
GIVEN:
- Operations that target outputs 0 and 2 but never 1
WHEN:
- They are validated
THEN:
- Three output documents are reported
"""
operations = [
{"page": 1, "doc": 0},
{"page": 2, "doc": 2},
{"page": 3, "doc": 0},
]
count = pdf_ops.validate_page_operations(operations, single_output=False)
assert count == 3
def test_empty_operations_are_rejected(self) -> None:
"""
GIVEN:
- No operations
WHEN:
- They are validated
THEN:
- ValueError is raised
"""
with pytest.raises(ValueError, match="index is out of bounds"):
pdf_ops.validate_page_operations([], single_output=False)
def test_multiple_outputs_rejected_when_single_output_required(self) -> None:
"""
GIVEN:
- Operations that target two outputs
WHEN:
- They are validated with a single output required
THEN:
- ValueError is raised
"""
operations = [{"page": 1, "doc": 0}, {"page": 2, "doc": 1}]
with pytest.raises(ValueError, match="Multiple output documents"):
pdf_ops.validate_page_operations(operations, single_output=True)
@pytest.mark.parametrize(
"doc",
[
pytest.param(-1, id="negative"),
pytest.param(2, id="equal-to-operation-count"),
pytest.param(2**32, id="huge"),
],
)
def test_output_index_out_of_bounds(self, doc: int) -> None:
"""
GIVEN:
- Two operations, one with an output index that is out of bounds
WHEN:
- They are validated
THEN:
- ValueError is raised
"""
operations = [{"page": 1, "doc": 0}, {"page": 2, "doc": doc}]
with pytest.raises(ValueError, match="index is out of bounds"):
pdf_ops.validate_page_operations(operations, single_output=False)
class TestDecrypt:
@pytest.mark.parametrize(
("path", "expected"),
[
pytest.param(ENCRYPTED, True, id="password-required"),
pytest.param(SIGNED, True, id="opens-without-password-but-encrypted"),
pytest.param(THREE_PAGES, False, id="not-encrypted"),
],
)
def test_needs_decrypt(self, path: Path, *, expected: bool) -> None:
"""
GIVEN:
- A PDF that is encrypted, or encrypted but openable, or plain
WHEN:
- needs_decrypt is asked about it
THEN:
- Only the unencrypted PDF reports False
"""
assert pdf_ops.needs_decrypt(path) is expected
def test_decrypt_writes_an_unencrypted_copy(self, tmp_path: Path) -> None:
"""
GIVEN:
- A password protected PDF
WHEN:
- It is decrypted with the correct password
THEN:
- The path from make_dst is returned
- The written copy no longer needs decrypting
"""
dst = tmp_path / "out.pdf"
result = pdf_ops.decrypt_pdf(ENCRYPTED, constant(dst), "test")
assert result == dst
assert pdf_ops.needs_decrypt(dst) is False
def test_wrong_password_raises_and_never_requests_a_destination(
self,
tmp_path: Path,
) -> None:
"""
GIVEN:
- A password protected PDF
WHEN:
- It is decrypted with the wrong password
THEN:
- PasswordError is raised
- No destination was requested
"""
requested: list[Path] = []
def make_dst() -> Path:
requested.append(tmp_path / "out.pdf")
return requested[-1]
with pytest.raises(pikepdf.PasswordError):
pdf_ops.decrypt_pdf(ENCRYPTED, make_dst, "wrong")
assert requested == []
class TestPdfMerger:
def test_pages_are_appended_in_the_order_added(
self,
tmp_path: Path,
source_fingerprints: list[str],
) -> None:
"""
GIVEN:
- A reordered PDF and the original three page PDF
WHEN:
- Both are added to a merger in that order and saved
THEN:
- The output holds all pages in the order they were added
"""
reordered = tmp_path / "reordered.pdf"
merged = tmp_path / "merged.pdf"
pdf_ops.build_pdfs(
THREE_PAGES,
[([PageSpec(3), PageSpec(1)], constant(reordered))],
)
with pdf_ops.PdfMerger() as merger:
merger.add(reordered)
merger.add(THREE_PAGES)
merger.save(merged)
assert fingerprints(merged) == [
source_fingerprints[2],
source_fingerprints[0],
*source_fingerprints,
]
def test_output_version_is_at_least_the_highest_source_version(
self,
tmp_path: Path,
) -> None:
"""
GIVEN:
- Two PDFs with different PDF versions
WHEN:
- Both are added to a merger and saved
THEN:
- The output version is at least the highest source version
"""
merged = tmp_path / "merged.pdf"
with pikepdf.open(TWELVE_PAGES) as pdf:
source_versions = [pdf.pdf_version]
with pikepdf.open(THREE_PAGES) as pdf:
source_versions.append(pdf.pdf_version)
with pdf_ops.PdfMerger() as merger:
merger.add(TWELVE_PAGES)
merger.add(THREE_PAGES)
merger.save(merged)
with pikepdf.open(merged) as pdf:
assert pdf.pdf_version >= max(source_versions)
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Afrikaans\n" "Language-Team: Afrikaans\n"
"Language: af_ZA\n" "Language: af_ZA\n"
@@ -1912,8 +1912,23 @@ msgstr ""
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "" msgstr ""
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Amharic\n" "Language-Team: Amharic\n"
"Language: am_ET\n" "Language: am_ET\n"
@@ -1912,8 +1912,23 @@ msgstr ""
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "" msgstr ""
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Arabic\n" "Language-Team: Arabic\n"
"Language: ar_SA\n" "Language: ar_SA\n"
@@ -1913,8 +1913,23 @@ msgstr "أنت على وشك استعمال حسابك في %(provider_name)s ل
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "كخطوة أخيرة، يرجى ملء النموذج التالي:" msgstr "كخطوة أخيرة، يرجى ملء النموذج التالي:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Belarusian\n" "Language-Team: Belarusian\n"
"Language: be_BY\n" "Language: be_BY\n"
@@ -1912,8 +1912,23 @@ msgstr ""
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "" msgstr ""
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Bulgarian\n" "Language-Team: Bulgarian\n"
"Language: bg_BG\n" "Language: bg_BG\n"
@@ -1913,8 +1913,23 @@ msgstr "На път сте да използвате профила си в %(pr
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Като последна стъпка, моля, попълнете следната форма:" msgstr "Като последна стъпка, моля, попълнете следната форма:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Catalan\n" "Language-Team: Catalan\n"
"Language: ca_ES\n" "Language: ca_ES\n"
@@ -1913,9 +1913,24 @@ msgstr "Estàs a punt d'emprar el teu %(provider_name)s compte per a fer login."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Com a pas final, completa el següent formulari:" msgstr "Com a pas final, completa el següent formulari:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "No es pot analitzar l'URI {value}, falta l'esquema"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "No es pot analitzar l'URI {value}, falta la ubicació de xarxa o la ruta"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "L'esquema d'URI '{parts.scheme}' no està permès. Esquemes permesos: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "No s'ha pogut analitzar l'URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+20 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Czech\n" "Language-Team: Czech\n"
"Language: cs_CZ\n" "Language: cs_CZ\n"
@@ -1913,9 +1913,25 @@ msgstr "Chystáte se použít svůj účet %(provider_name)s k přihlášení."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Jako poslední krok vyplňte následující formulář:" msgstr "Jako poslední krok vyplňte následující formulář:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Nelze analyzovat URI {value}, chybí schéma"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Nelze analyzovat URI {value}, chybí síťové umístění nebo cesta"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "URI schéma '{parts.scheme}' není povoleno. Povolená schémata: {',\n"
"'.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Nelze zpracovat URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Danish\n" "Language-Team: Danish\n"
"Language: da_DK\n" "Language: da_DK\n"
@@ -1913,8 +1913,23 @@ msgstr "En %(provider_name)s-konto er ved at blive brugt til indlogning."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Som sidste trin, udfyld venligst flg. formular:" msgstr "Som sidste trin, udfyld venligst flg. formular:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German, Switzerland\n" "Language-Team: German, Switzerland\n"
"Language: de_CH\n" "Language: de_CH\n"
@@ -1913,9 +1913,24 @@ msgstr "Sie sind dabei, Ihr %(provider_name)s-Konto zur Anmeldung zu verwenden."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Bitte füllen Sie zum Abschluss folgendes Formular aus:" msgstr "Bitte füllen Sie zum Abschluss folgendes Formular aus:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "URI {value} kann nicht gelesen werden, fehlendes Schema"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "URI {value} kann nicht gelesen werden, Netzadresse oder Pfad fehlt"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "URI-Schema „{parts.scheme}“ ist nicht erlaubt. Erlaubte Schemata: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "URI {value} kann nicht gelesen werden"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-07 00:59\n" "PO-Revision-Date: 2026-10-06 00:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German\n" "Language-Team: German\n"
"Language: de_DE\n" "Language: de_DE\n"
@@ -1913,9 +1913,24 @@ msgstr "Sie sind dabei, Ihr %(provider_name)s-Konto zur Anmeldung zu verwenden."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Bitte füllen Sie zum Abschluss folgendes Formular aus:" msgstr "Bitte füllen Sie zum Abschluss folgendes Formular aus:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "URI {value} kann nicht gelesen werden, fehlendes Schema"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "URI {value} kann nicht gelesen werden, Netzadresse oder Pfad fehlt"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "URI-Schema „{parts.scheme}“ ist nicht erlaubt. Erlaubte Schemata: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "URI {value} kann nicht gelesen werden"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Greek\n" "Language-Team: Greek\n"
"Language: el_GR\n" "Language: el_GR\n"
@@ -1913,8 +1913,23 @@ msgstr "Πρόκειται να χρησιμοποιήσετε το λογαρι
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Ως τελικό βήμα, παρακαλούμε να συμπληρώσετε την ακόλουθη φόρμα:" msgstr "Ως τελικό βήμα, παρακαλούμε να συμπληρώσετε την ακόλουθη φόρμα:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Spanish\n" "Language-Team: Spanish\n"
"Language: es_ES\n" "Language: es_ES\n"
@@ -1913,9 +1913,24 @@ msgstr "Estás a punto de usar tu cuenta de %(provider_name)s para iniciar sesi
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Como último paso, por favor complete el siguiente formulario:" msgstr "Como último paso, por favor complete el siguiente formulario:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "No se puede analizar la URI {value}, falta el esquema"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "No se puede analizar la URI {value}, falta la ubicación de la red o la ruta"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "El esquema URI '{parts.scheme}' no está permitido. Esquemas permitidos: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "No se puede analizar la URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Estonian\n" "Language-Team: Estonian\n"
"Language: et_EE\n" "Language: et_EE\n"
@@ -1912,8 +1912,23 @@ msgstr ""
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "" msgstr ""
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Persian\n" "Language-Team: Persian\n"
"Language: fa_IR\n" "Language: fa_IR\n"
@@ -1912,8 +1912,23 @@ msgstr ""
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "به عنوان مرحله آخر ، لطفاً فرم زیر را تکمیل کنید:" msgstr "به عنوان مرحله آخر ، لطفاً فرم زیر را تکمیل کنید:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Finnish\n" "Language-Team: Finnish\n"
"Language: fi_FI\n" "Language: fi_FI\n"
@@ -1913,8 +1913,23 @@ msgstr ""
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "" msgstr ""
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: French\n" "Language-Team: French\n"
"Language: fr_FR\n" "Language: fr_FR\n"
@@ -1912,9 +1912,24 @@ msgstr "Vous vous apprêtez à utiliser votre compte %(provider_name)s pour vous
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Pour finir, veuillez remplir le formulaire suivant :" msgstr "Pour finir, veuillez remplir le formulaire suivant :"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Impossible d'analyser l'URI {value}, schéma manquant"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Impossible d'analyser l'URI {value}, emplacement réseau ou chemin manquant"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "Le schéma d'URI « {parts.scheme} » n'est pas autorisé. Schémas autorisés : {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Impossible d'analyser l'URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hebrew\n" "Language-Team: Hebrew\n"
"Language: he_IL\n" "Language: he_IL\n"
@@ -1914,9 +1914,24 @@ msgstr "שלך כדי להתחבר %(provider_name)s אתה עומד להשתמ
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr ": כצעד אחרון, אנא מלא את הטופס הבא:" msgstr ": כצעד אחרון, אנא מלא את הטופס הבא:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "לא ניתן לפענח את ה-URI ‏{value}, חסר פרוטוקול (scheme)"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "לא ניתן לפענח את ה-URI ‏{value}, חסר מיקום רשת (net location) או נתיב"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "פרוטוקול ה-URI ‏'{parts.scheme}' אינו מורשה. הפרוטוקולים המורשים הם: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "לא ניתן לפענח את ה URI ‏{value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hindi\n" "Language-Team: Hindi\n"
"Language: hi_IN\n" "Language: hi_IN\n"
@@ -1912,8 +1912,23 @@ msgstr ""
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "" msgstr ""
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Croatian\n" "Language-Team: Croatian\n"
"Language: hr_HR\n" "Language: hr_HR\n"
@@ -1913,9 +1913,24 @@ msgstr "Upravo ćete koristiti svoj račun %(provider_name)s za prijavu."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Kao posljednji korak ispunite sljedeći obrazac:" msgstr "Kao posljednji korak ispunite sljedeći obrazac:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Nije moguće analizirati URI {value}, nedostaje shema"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Nije moguće analizirati URI {value}, nedostaje mrežna lokacija ili put"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "URI shema '{parts.scheme}' nije dopuštena. Dopuštene sheme: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Nije moguće raščlaniti URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hungarian\n" "Language-Team: Hungarian\n"
"Language: hu_HU\n" "Language: hu_HU\n"
@@ -1913,9 +1913,24 @@ msgstr "A %(provider_name)s felhasználói fiókkal fogsz belépni."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Utolsó lépésként kérjük válaszoljon az alábbi kérdésekre:" msgstr "Utolsó lépésként kérjük válaszoljon az alábbi kérdésekre:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "A {value} URI értelmezése sikertelen, hiányzó séma"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "A {value} URI értelmezése sikertelen, hiányzó hálózati vagy elérési útvonal"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "A '{parts.scheme}' séma nem engedélyezett. Engedélyezett sémák: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "A {value} URI értelmezése sikertelen"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Indonesian\n" "Language-Team: Indonesian\n"
"Language: id_ID\n" "Language: id_ID\n"
@@ -1913,9 +1913,24 @@ msgstr "Anda akan menggunakan akun %(provider_name)s Anda untuk masuk."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Sebagai langkah terakhir, mohon formulir berikut:" msgstr "Sebagai langkah terakhir, mohon formulir berikut:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Gagal membaca URI {value}, skema tidak ditemukan"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Gagal membaca URI {value}, lokasi jaringan atau path tidak ditemukan"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "Skema URI '{parts.scheme}' tidak diizinkan. Skema yang diizinkan: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Gagal membaca URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+27 -12
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Italian\n" "Language-Team: Italian\n"
"Language: it_IT\n" "Language: it_IT\n"
@@ -253,7 +253,7 @@ msgstr "documento radice per questa versione"
#: documents/models.py:325 #: documents/models.py:325
msgid "version index" msgid "version index"
msgstr "indice versione" msgstr "indice della versione"
#: documents/models.py:329 #: documents/models.py:329
msgid "Index of this version within the root document." msgid "Index of this version within the root document."
@@ -261,11 +261,11 @@ msgstr "Indice di questa versione all'interno del documento radice."
#: documents/models.py:333 #: documents/models.py:333
msgid "version label" msgid "version label"
msgstr "etichetta versione" msgstr "etichetta della versione"
#: documents/models.py:337 #: documents/models.py:337
msgid "Optional short label for a document version." msgid "Optional short label for a document version."
msgstr "Etichetta abbreviata facoltativa per una versione del documento." msgstr "Etichetta breve facoltativa per una versione del documento."
#: documents/models.py:342 documents/models.py:961 documents/models.py:992 #: documents/models.py:342 documents/models.py:961 documents/models.py:992
#: documents/models.py:1096 documents/models.py:2126 #: documents/models.py:1096 documents/models.py:2126
@@ -1006,11 +1006,11 @@ msgstr "note"
#: documents/models.py:996 #: documents/models.py:996
msgid "page" msgid "page"
msgstr "pagina" msgstr ""
#: documents/models.py:997 #: documents/models.py:997
msgid "Page of the original file, starting at 1" msgid "Page of the original file, starting at 1"
msgstr "Pagina del file originale, partendo da 1" msgstr ""
#: documents/models.py:1052 #: documents/models.py:1052
msgid "format" msgid "format"
@@ -1018,11 +1018,11 @@ msgstr ""
#: documents/models.py:1056 #: documents/models.py:1056
msgid "document barcode" msgid "document barcode"
msgstr "codice a barre del documento" msgstr ""
#: documents/models.py:1057 #: documents/models.py:1057
msgid "document barcodes" msgid "document barcodes"
msgstr "codici a barre del documento" msgstr ""
#: documents/models.py:1066 #: documents/models.py:1066
msgid "Original" msgid "Original"
@@ -1913,9 +1913,24 @@ msgstr "Stai per utilizzare il tuo account %(provider_name)s per effettuare il l
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Come ultimo passo, compila il seguente modulo:" msgstr "Come ultimo passo, compila il seguente modulo:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Impossibile analizzare l'URI {value}, schema mancante"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Impossibile analizzare l'URI {value}, posizione o percorso di rete mancante"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "Lo schema URI '{parts.scheme}' non è consentito. Schemi consentiti: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Impossibile analizzare l'URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
@@ -2168,7 +2183,7 @@ msgstr "Abilita la suddivisione sui codici a barre dei tag"
#: paperless/models.py:308 #: paperless/models.py:308
msgid "Stores the values of detected barcodes" msgid "Stores the values of detected barcodes"
msgstr "Memorizza i valori dei codici a barre individuati" msgstr ""
#: paperless/models.py:318 #: paperless/models.py:318
msgid "Sets the remote OCR engine" msgid "Sets the remote OCR engine"
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
"Language: ja_JP\n" "Language: ja_JP\n"
@@ -1913,8 +1913,23 @@ msgstr "%(provider_name)s アカウントでログインしようとしていま
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "最後のステップとして、以下のフォームに記入してください" msgstr "最後のステップとして、以下のフォームに記入してください"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Korean\n" "Language-Team: Korean\n"
"Language: ko_KR\n" "Language: ko_KR\n"
@@ -1913,9 +1913,24 @@ msgstr "%(provider_name)s 의 계정을 사용하여 로그인하려고 합니
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "마지막 단계로 다음 양식을 작성해 주세요:" msgstr "마지막 단계로 다음 양식을 작성해 주세요:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "{value} URI를 파싱할 수 없습니다: 스킴이 누락되었습니다."
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "{value} URI를 파싱할 수 없습니다: 네트워크 위치 또는 경로가 누락되었습니다."
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "URI 스킴 '{parts.scheme}'는 허용되지 않습니다. 허용된 스킴: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "{value} URI를 파싱할 수 없음"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Luxembourgish\n" "Language-Team: Luxembourgish\n"
"Language: lb_LU\n" "Language: lb_LU\n"
@@ -1912,8 +1912,23 @@ msgstr ""
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "" msgstr ""
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Lithuanian\n" "Language-Team: Lithuanian\n"
"Language: lt_LT\n" "Language: lt_LT\n"
@@ -1912,9 +1912,24 @@ msgstr "Ketinate prisijungimui naudoti savo %(provider_name)s paskyrą."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Galiausiai užpildykite šią formą:" msgstr "Galiausiai užpildykite šią formą:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Nepavyko apdoroti URI {value}, trūksta schemos"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Nepavyko apdoroti URI {value}, trūksta tinklo vietos arba kelio"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "URI schema '{parts.scheme}' nėra leistina. Galimos schemos: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Nepavyko apdoroti URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Latvian\n" "Language-Team: Latvian\n"
"Language: lv_LV\n" "Language: lv_LV\n"
@@ -1912,8 +1912,23 @@ msgstr ""
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "" msgstr ""
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Macedonian\n" "Language-Team: Macedonian\n"
"Language: mk_MK\n" "Language: mk_MK\n"
@@ -1912,8 +1912,23 @@ msgstr ""
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "" msgstr ""
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Malay\n" "Language-Team: Malay\n"
"Language: ms_MY\n" "Language: ms_MY\n"
@@ -1912,8 +1912,23 @@ msgstr ""
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "" msgstr ""
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Dutch\n" "Language-Team: Dutch\n"
"Language: nl_NL\n" "Language: nl_NL\n"
@@ -1913,8 +1913,23 @@ msgstr "Je staat op het punt om met je %(provider_name)s account in te loggen."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Vul als laatste stap het volgende formulier in:" msgstr "Vul als laatste stap het volgende formulier in:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Norwegian\n" "Language-Team: Norwegian\n"
"Language: no_NO\n" "Language: no_NO\n"
@@ -1913,9 +1913,24 @@ msgstr "You are about to use your %(provider_name)s account to login. (NO)"
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "As a final step, please complete the following form: (NO)" msgstr "As a final step, please complete the following form: (NO)"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Unable to parse URI {value}, missing scheme (NO)"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Unable to parse URI {value}, missing net location or path (NO)"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)} (NO)"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Unable to parse URI {value} (NO)"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Polish\n" "Language-Team: Polish\n"
"Language: pl_PL\n" "Language: pl_PL\n"
@@ -1913,9 +1913,24 @@ msgstr "Zamierzasz użyć konta %(provider_name)s aby się zalogować."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Na koniec należy wypełnić poniższy formularz:" msgstr "Na koniec należy wypełnić poniższy formularz:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Nie można przeanalizować URI {value}, brak schematu"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Nie można przeanalizować URI {value}, brak lokalizacji sieciowej lub ścieżki"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "Schemat URI '{parts.scheme}' jest niedozwolony. Dozwolone schematy: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Nie można przetworzyć URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+20 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Portuguese, Brazilian\n" "Language-Team: Portuguese, Brazilian\n"
"Language: pt_BR\n" "Language: pt_BR\n"
@@ -1914,10 +1914,26 @@ msgstr "Você está prestes a usar sua conta %(provider_name)s para entrar."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Como etapa final, por favor preencha o seguinte formulário:" msgstr "Como etapa final, por favor preencha o seguinte formulário:"
#: documents/validators.py:36 #: documents/validators.py:24
msgid ", " #, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Não é possível analisar o URI {value}, esquema ausente"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "" msgstr ""
#: documents/validators.py:36
msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "Esquema URI '{parts.scheme}' não é permitido. Esquemas permitidos:\n"
"{', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Não é possível analisar o URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "more_like_id inválido" msgstr "more_like_id inválido"
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Portuguese\n" "Language-Team: Portuguese\n"
"Language: pt_PT\n" "Language: pt_PT\n"
@@ -1913,10 +1913,25 @@ msgstr ""
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "" msgstr ""
#: documents/validators.py:36 #: documents/validators.py:24
msgid ", " #, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Não foi possível analisar o URI {value}, o esquema está em falta"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "" msgstr ""
#: documents/validators.py:36
msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Não foi possível analisar o URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Romanian\n" "Language-Team: Romanian\n"
"Language: ro_RO\n" "Language: ro_RO\n"
@@ -1913,9 +1913,24 @@ msgstr "Sunteți pe cale să vă utilizați contul %(provider_name)s pentru a v
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "În etapa finală, vă rugăm să completați următorul formular:" msgstr "În etapa finală, vă rugăm să completați următorul formular:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Imposibil de analizat URI {value}, schemă lipsă"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Imposibil de analizat URI {value}, lipsă locație sau cale"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "Schema URI '{parts.scheme}' nu este permisă. Schemele permise: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Nu se poate analiza URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Russian\n" "Language-Team: Russian\n"
"Language: ru_RU\n" "Language: ru_RU\n"
@@ -1913,9 +1913,24 @@ msgstr "Вы собираетесь войти через сторонний с
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "На завершение, пожалуйста заполните следующую форму:" msgstr "На завершение, пожалуйста заполните следующую форму:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Не удается распознать URI {value}, отсутствует схема"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Не удалось распознать URI {value}, отсутствует сетевое расположение или путь"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "Недопустимая схема URI '{parts.scheme}'. Разрешенные схемы: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Невозможно распознать URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-07 12:33\n" "PO-Revision-Date: 2026-10-06 02:00\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Slovak\n" "Language-Team: Slovak\n"
"Language: sk_SK\n" "Language: sk_SK\n"
@@ -1913,9 +1913,24 @@ msgstr "Chystáte sa na prihlásenie použiť svoj účet %(provider_name)s."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Ako posledný krok vyplňte nasledujúci formulár:" msgstr "Ako posledný krok vyplňte nasledujúci formulár:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Nepodarilo sa analyzovať URI {value}, chýba schéma"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Nepodarilo sa analyzovať URI {value}; chýba sieťové umiestnenie alebo cesta."
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "Schéma URI „{parts.scheme}“ nie je povolená. Povolené schémy: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Nepodarilo sa analyzovať URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Slovenian\n" "Language-Team: Slovenian\n"
"Language: sl_SI\n" "Language: sl_SI\n"
@@ -1913,9 +1913,24 @@ msgstr "Za prijavo boste uporabili račun ponudnika %(provider_name)s."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Kot zadnji korak vas prosimo, da izpolnite naslednji obrazec:" msgstr "Kot zadnji korak vas prosimo, da izpolnite naslednji obrazec:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Ni mogoče razčleniti URI-ja {value}, manjka shema"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Ni mogoče razčleniti URI-ja {value}, manjka omrežna lokacija ali pot"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "Shema URI '{parts.scheme}' ni dovoljena. Dovoljene sheme: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Ni mogoče razčleniti URI-ja {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Albanian\n" "Language-Team: Albanian\n"
"Language: sq_AL\n" "Language: sq_AL\n"
@@ -1913,8 +1913,23 @@ msgstr "You are about to use your %(provider_name)s llogari to login."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Si hap i fundit, ju lutemi plotësoni formularin e mëposhtëm:" msgstr "Si hap i fundit, ju lutemi plotësoni formularin e mëposhtëm:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Serbian (Latin)\n" "Language-Team: Serbian (Latin)\n"
"Language: sr_CS\n" "Language: sr_CS\n"
@@ -1913,9 +1913,24 @@ msgstr "Uskoro ćete koristiti svoj nalog na %(provider_name)s za prijavu."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Kao poslednji korak, molimo Vas da popunite sledeći obrazac:" msgstr "Kao poslednji korak, molimo Vas da popunite sledeći obrazac:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Nije moguće analizirati URI {value}, nedostaje šema"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Nije moguće analizirati URI {value}, nedostaje mrežan lokacija ili putanja"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "Šema URI-ja '{parts.scheme}' nije dozvoljena. Dozvoljene šeme {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Nije moguće analizirati URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Swedish\n" "Language-Team: Swedish\n"
"Language: sv_SE\n" "Language: sv_SE\n"
@@ -1913,9 +1913,24 @@ msgstr "Du håller på att använda ditt %(provider_name)s-konto för att logga
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Slutför följande formulär som ett sista steg:" msgstr "Slutför följande formulär som ett sista steg:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Kunde inte tolka URI {value}, schema saknas"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Kunde inte tolka URI {value}, nätverksplats eller sökväg saknas"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "URI-schemat '{parts.scheme}' är inte tillåtet. Tillåtna scheman: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Kunde inte tolka URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Thai\n" "Language-Team: Thai\n"
"Language: th_TH\n" "Language: th_TH\n"
@@ -1912,8 +1912,23 @@ msgstr ""
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "" msgstr ""
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Turkish\n" "Language-Team: Turkish\n"
"Language: tr_TR\n" "Language: tr_TR\n"
@@ -1913,9 +1913,24 @@ msgstr "Giriş yapmak için %(provider_name)s hesabınızı kullanmak üzeresini
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Son adım olarak, lütfen aşağıdaki formu doldurun:" msgstr "Son adım olarak, lütfen aşağıdaki formu doldurun:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "URI {value} ayrıştırılamıyor, şema eksik"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "URI {value} ayrıştırılamıyor, ağ konumu veya yol eksik"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "URI şeması ‘{parts.scheme}’ izin verilmez. İzin verilen şemalar: {‘, ’.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "URI {value} işlenemiyor"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+18 -3
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Ukrainian\n" "Language-Team: Ukrainian\n"
"Language: uk_UA\n" "Language: uk_UA\n"
@@ -1913,8 +1913,23 @@ msgstr "Ви збираєтеся використовувати ваш облі
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Як останній крок, будь ласка, заповніть наступну форму:" msgstr "Як останній крок, будь ласка, заповніть наступну форму:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Vietnamese\n" "Language-Team: Vietnamese\n"
"Language: vi_VN\n" "Language: vi_VN\n"
@@ -1913,9 +1913,24 @@ msgstr "Bạn sắp sử dụng tài khoản %(provider_name)s của bạn để
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "Là bước cuối cùng, vui lòng hoàn thành mẫu đơn sau:" msgstr "Là bước cuối cùng, vui lòng hoàn thành mẫu đơn sau:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "Không thể phân tích URI {value}, thiếu giao thức"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "Không thể phân tích URI {value}, thiếu tên miền hoặc đường dẫn"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "Giao thức URI '{parts.scheme}' không được phép. Các giao thức được phép: {', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "Không thể phân tích URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Chinese Simplified\n" "Language-Team: Chinese Simplified\n"
"Language: zh_CN\n" "Language: zh_CN\n"
@@ -1913,9 +1913,24 @@ msgstr "您将使用您的 %(provider_name)s 账户登录."
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "作为最后一步,请填写以下表格:" msgstr "作为最后一步,请填写以下表格:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "无法解析 URI {value},缺少scheme"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "无法解析 URI {value},缺少网络位置或路径"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "URI 格式 '{parts.scheme}' 不支持。支持的格式有:{', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "无法解析 URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
+19 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-06 15:12+0000\n" "POT-Creation-Date: 2026-10-05 16:26+0000\n"
"PO-Revision-Date: 2026-10-06 15:13\n" "PO-Revision-Date: 2026-10-05 16:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Chinese Traditional\n" "Language-Team: Chinese Traditional\n"
"Language: zh_TW\n" "Language: zh_TW\n"
@@ -1913,9 +1913,24 @@ msgstr "即將使用 %(provider_name)s 帳號登入。"
msgid "As a final step, please complete the following form:" msgid "As a final step, please complete the following form:"
msgstr "最後一步,請填寫以下表單:" msgstr "最後一步,請填寫以下表單:"
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr "無法解析 URI {value},缺少協定"
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr "無法解析 URI {value},缺少網路位置或路徑"
#: documents/validators.py:36 #: documents/validators.py:36
msgid ", " msgid "URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}"
msgstr "" msgstr "URI 協定「{parts.scheme}」不被允許。允許的協定有:{', '.join(allowed_schemes)}"
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr "無法解析 URI {value}"
#: documents/views.py:336 documents/views.py:2729 #: documents/views.py:336 documents/views.py:2729
msgid "Invalid more_like_id" msgid "Invalid more_like_id"