Merge remote-tracking branch 'origin/dev'

This commit is contained in:
Trenton Holmes 2023-12-15 17:05:16 -08:00
commit c4b7429e99
22 changed files with 589 additions and 629 deletions

View File

@ -733,7 +733,7 @@ they use underscores instead of dashes.
Paperless has been tested to work with the OCR options provided
above. There are many options that are incompatible with each other,
so specifying invalid options may prevent paperless from consuming
any documents.
any documents. Use with caution!
Specify arguments as a JSON dictionary. Keep note of lower case
booleans and double quoted parameter names and strings. Examples:

View File

@ -17,7 +17,7 @@
<div class="col-md-4">
<h5 class="border-bottom pb-2" i18n>Filters</h5>
<p class="small" i18n>Process documents that match <em>all</em> filters specified below.</p>
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.filter_filename"></pngx-input-select>
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case insensitive.</a>" [error]="error?.filter_path"></pngx-input-text>
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>

View File

@ -9,7 +9,7 @@
</button>
</div>
<div [class.col-md-9]="horizontal">
<div [class.input-group]="allowCreateNew || showFilter">
<div [class.input-group]="allowCreateNew || showFilter" [class.is-invalid]="error">
<ng-select name="inputId" [(ngModel)]="value"
[disabled]="disabled"
[style.color]="textColor"
@ -42,6 +42,9 @@
</svg>
</button>
</div>
<div class="invalid-feedback">
{{error}}
</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
<small *ngIf="getSuggestions().length > 0">
<span i18n>Suggestions:</span>&nbsp;

View File

@ -17,3 +17,12 @@
font-style: italic;
opacity: .75;
}
::ng-deep .is-invalid ng-select .ng-select-container input {
// replicate bootstrap
padding-right: calc(1.5em + 0.75rem) !important;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") !important;
background-repeat: no-repeat !important;
background-position: right calc(0.375em + 0.1875rem) center !important;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem) !important;
}

View File

@ -5,7 +5,7 @@ export const environment = {
apiBaseUrl: document.baseURI + 'api/',
apiVersion: '3',
appTitle: 'Paperless-ngx',
version: '2.1.2',
version: '2.1.2-dev',
webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/',

View File

@ -10,13 +10,13 @@
</context-group>
<target state="final">Fermer</target>
</trans-unit>
<trans-unit id="ngb.timepicker.HH" datatype="html">
<trans-unit id="ngb.timepicker.HH" datatype="html" approved="yes">
<source>HH</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context>
</context-group>
<target state="translated">HH</target>
<target state="final">HH</target>
</trans-unit>
<trans-unit id="ngb.toast.close-aria" datatype="html" approved="yes">
<source>Close</source>
@ -100,13 +100,13 @@
</context-group>
<target state="final">Précédent</target>
</trans-unit>
<trans-unit id="ngb.timepicker.MM" datatype="html">
<trans-unit id="ngb.timepicker.MM" datatype="html" approved="yes">
<source>MM</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context>
</context-group>
<target state="translated">MM</target>
<target state="final">MM</target>
</trans-unit>
<trans-unit id="ngb.pagination.next" datatype="html" approved="yes">
<source>»</source>

View File

@ -288,7 +288,7 @@
<context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">90</context>
</context-group>
<target state="needs-translation">Document <x id="PH" equiv-text="status.filename"/> was added to Paperless-ngx.</target>
<target state="translated">Dokument <x id="PH" equiv-text="status.filename"/> je dodan u Paperless-ngx.</target>
</trans-unit>
<trans-unit id="1931214133925051574" datatype="html">
<source>Open document</source>
@ -316,7 +316,7 @@
<context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">120</context>
</context-group>
<target state="needs-translation">Document <x id="PH" equiv-text="status.filename"/> is being processed by Paperless-ngx.</target>
<target state="translated">Dokument <x id="PH" equiv-text="status.filename"/> je u fazi obrade.</target>
</trans-unit>
<trans-unit id="2501522447884928778" datatype="html">
<source>Prev</source>
@ -476,7 +476,7 @@
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">15</context>
</context-group>
<target state="needs-translation">Auto refresh</target>
<target state="translated">Automatsko osvježavanje</target>
</trans-unit>
<trans-unit id="3894950702316166331" datatype="html">
<source>Loading...</source>
@ -596,7 +596,7 @@
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">15</context>
</context-group>
<target state="needs-translation">General</target>
<target state="translated">Općenito</target>
</trans-unit>
<trans-unit id="8671234314555525900" datatype="html">
<source>Appearance</source>
@ -916,7 +916,7 @@
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">178,180</context>
</context-group>
<target state="needs-translation"> Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI </target>
<target state="translated"> Postavke ovog korisničkog računa za objekte (Oznake, Pravila za e-poštu, itd.) stvorene putem web sučelja </target>
</trans-unit>
<trans-unit id="4292903881380648974" datatype="html">
<source>Default Owner</source>
@ -2074,7 +2074,7 @@
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">124</context>
</context-group>
<target state="needs-translation">Deleted user</target>
<target state="translated">Izbrisani korisnik</target>
</trans-unit>
<trans-unit id="1942566571910298572" datatype="html">
<source>Error deleting user.</source>
@ -2479,7 +2479,7 @@
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
<context context-type="linenumber">276</context>
</context-group>
<target state="needs-translation">An error occurred while saving update checking settings.</target>
<target state="translated">Došlo je do pogreške prilikom spremanja postavki ažuriranja.</target>
</trans-unit>
<trans-unit id="8700121026680200191" datatype="html">
<source>Clear</source>
@ -3039,7 +3039,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">9</context>
</context-group>
<target state="needs-translation">Data type</target>
<target state="translated">Tip podataka</target>
</trans-unit>
<trans-unit id="5933665691581884232" datatype="html">
<source>Data type cannot be changed after a field is created</source>
@ -3047,7 +3047,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">10</context>
</context-group>
<target state="needs-translation">Data type cannot be changed after a field is created</target>
<target state="translated">Tip podataka ne može se promijeniti nakon što je polje stvoreno</target>
</trans-unit>
<trans-unit id="528950215505228201" datatype="html">
<source>Create new custom field</source>
@ -3175,7 +3175,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.html</context>
<context context-type="linenumber">19</context>
</context-group>
<target state="needs-translation">Character Set</target>
<target state="translated">Skup znakova</target>
</trans-unit>
<trans-unit id="6563391987554512024" datatype="html">
<source>Test</source>

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,8 @@ from pikepdf import Pdf
from PIL import Image
from documents.converters import convert_from_tiff_to_pdf
from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides
from documents.data_models import DocumentSource
from documents.utils import copy_basic_file_stats
from documents.utils import copy_file_with_basic_stats
@ -53,6 +55,7 @@ class BarcodeReader:
self.mime: Final[str] = mime_type
self.pdf_file: Path = self.file
self.barcodes: list[Barcode] = []
self._tiff_conversion_done = False
self.temp_dir: Optional[tempfile.TemporaryDirectory] = None
if settings.CONSUMER_BARCODE_TIFF_SUPPORT:
@ -150,12 +153,14 @@ class BarcodeReader:
def convert_from_tiff_to_pdf(self):
"""
May convert a TIFF image into a PDF, if the input is a TIFF
May convert a TIFF image into a PDF, if the input is a TIFF and
the TIFF has not been made into a PDF
"""
# Nothing to do, pdf_file is already assigned correctly
if self.mime != "image/tiff":
if self.mime != "image/tiff" or self._tiff_conversion_done:
return
self._tiff_conversion_done = True
self.pdf_file = convert_from_tiff_to_pdf(self.file, Path(self.temp_dir.name))
def detect(self) -> None:
@ -167,6 +172,9 @@ class BarcodeReader:
if self.barcodes:
return
# No op if not a TIFF
self.convert_from_tiff_to_pdf()
# Choose the library for reading
if settings.CONSUMER_BARCODE_SCANNER == "PYZBAR":
reader = self.read_barcodes_pyzbar
@ -240,7 +248,7 @@ class BarcodeReader:
"""
document_paths = []
fname = self.file.with_suffix("").name
fname = self.file.stem
with Pdf.open(self.pdf_file) as input_pdf:
# Start with an empty document
current_document: list[Page] = []
@ -290,7 +298,7 @@ class BarcodeReader:
def separate(
self,
source: DocumentSource,
override_name: Optional[str] = None,
overrides: DocumentMetadataOverrides,
) -> bool:
"""
Separates the document, based on barcodes and configuration, creating new
@ -316,27 +324,23 @@ class BarcodeReader:
logger.warning("No pages to split on!")
return False
# Create the split documents
doc_paths = self.separate_pages(separator_pages)
tmp_dir = Path(tempfile.mkdtemp(prefix="paperless-barcode-split-")).resolve()
# Save the new documents to correct folder
if source != DocumentSource.ConsumeFolder:
# The given file is somewhere in SCRATCH_DIR,
# and new documents must be moved to the CONSUMPTION_DIR
# for the consumer to notice them
save_to_dir = settings.CONSUMPTION_DIR
else:
# The given file is somewhere in CONSUMPTION_DIR,
# and may be some levels down for recursive tagging
# so use the file's parent to preserve any metadata
save_to_dir = self.file.parent
from documents import tasks
for idx, document_path in enumerate(doc_paths):
if override_name is not None:
newname = f"{idx}_{override_name}"
dest = save_to_dir / newname
else:
dest = save_to_dir
logger.info(f"Saving {document_path} to {dest}")
copy_file_with_basic_stats(document_path, dest)
# Create the split document tasks
for new_document in self.separate_pages(separator_pages):
copy_file_with_basic_stats(new_document, tmp_dir / new_document.name)
tasks.consume_file.delay(
ConsumableDocument(
# Same source, for templates
source=source,
# Can't use same folder or the consume might grab it again
original_file=(tmp_dir / new_document.name).resolve(),
),
# All the same metadata
overrides,
)
logger.info("Barcode splitting complete!")
return True

View File

@ -3,6 +3,7 @@ import math
import os
from collections import Counter
from contextlib import contextmanager
from datetime import datetime
from typing import Optional
from dateutil.parser import isoparse
@ -371,7 +372,7 @@ class LocalDateParser(English):
if isinstance(d, timespan):
d.start = self.reverse_timezone_offset(d.start)
d.end = self.reverse_timezone_offset(d.end)
else:
elif isinstance(d, datetime):
d = self.reverse_timezone_offset(d)
return d

View File

@ -238,18 +238,6 @@ class Command(BaseCommand):
serializers.serialize("json", StoragePath.objects.all()),
)
notes = json.loads(
serializers.serialize("json", Note.objects.all()),
)
if not self.split_manifest:
manifest += notes
documents = Document.objects.order_by("id")
document_map = {d.pk: d for d in documents}
document_manifest = json.loads(serializers.serialize("json", documents))
if not self.split_manifest:
manifest += document_manifest
manifest += json.loads(
serializers.serialize("json", MailAccount.objects.all()),
)
@ -303,10 +291,24 @@ class Command(BaseCommand):
serializers.serialize("json", CustomField.objects.all()),
)
# These are treated specially and included in the per-document manifest
# if that setting is enabled. Otherwise, they are just exported to the bulk
# manifest
documents = Document.objects.order_by("id")
document_map: dict[int, Document] = {d.pk: d for d in documents}
document_manifest = json.loads(serializers.serialize("json", documents))
notes = json.loads(
serializers.serialize("json", Note.objects.all()),
)
custom_field_instances = json.loads(
serializers.serialize("json", CustomFieldInstance.objects.all()),
)
if not self.split_manifest:
manifest += json.loads(
serializers.serialize("json", CustomFieldInstance.objects.all()),
)
manifest += document_manifest
manifest += notes
manifest += custom_field_instances
# 3. Export files from each document
for index, document_dict in tqdm.tqdm(
@ -412,6 +414,12 @@ class Command(BaseCommand):
notes,
),
)
content += list(
filter(
lambda d: d["fields"]["document"] == document_dict["pk"],
custom_field_instances,
),
)
manifest_name.write_text(
json.dumps(content, indent=2, ensure_ascii=False),
encoding="utf-8",

View File

@ -140,7 +140,7 @@ def consume_file(
with BarcodeReader(input_doc.original_file, input_doc.mime_type) as reader:
if settings.CONSUMER_ENABLE_BARCODES and reader.separate(
input_doc.source,
overrides.filename,
overrides,
):
# notify the sender, otherwise the progress bar
# in the UI stays stuck

View File

@ -455,6 +455,31 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
# Assert subset in results
self.assertDictEqual(result, {**result, **subset})
def test_search_added_invalid_date(self):
"""
GIVEN:
- One document added right now
WHEN:
- Query with invalid added date
THEN:
- No documents returned
"""
d1 = Document.objects.create(
title="invoice",
content="the thing i bought at a shop and paid with bank account",
checksum="A",
pk=1,
)
with index.open_index_writer() as writer:
index.update_document(writer, d1)
response = self.client.get("/api/documents/?query=added:invalid-date")
results = response.data["results"]
# Expect 0 document returned
self.assertEqual(len(results), 0)
@mock.patch("documents.index.autocomplete")
def test_search_autocomplete_limits(self, m):
"""

View File

@ -1,5 +1,4 @@
import shutil
from pathlib import Path
from unittest import mock
import pytest
@ -11,10 +10,13 @@ from documents import tasks
from documents.barcodes import BarcodeReader
from documents.consumer import ConsumerError
from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides
from documents.data_models import DocumentSource
from documents.models import Document
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import DocumentConsumeDelayMixin
from documents.tests.utils import FileSystemAssertsMixin
from documents.tests.utils import SampleDirMixin
try:
import zxingcpp # noqa: F401
@ -25,11 +27,7 @@ except ImportError:
@override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR")
class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
SAMPLE_DIR = Path(__file__).parent / "samples"
BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes"
class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, TestCase):
def test_scan_file_for_separating_barcodes(self):
"""
GIVEN:
@ -48,6 +46,46 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertEqual(reader.pdf_file, test_file)
self.assertDictEqual(separator_page_numbers, {0: False})
@override_settings(
CONSUMER_BARCODE_TIFF_SUPPORT=True,
)
def test_scan_tiff_for_separating_barcodes(self):
"""
GIVEN:
- TIFF image containing barcodes
WHEN:
- Consume task returns
THEN:
- The file was split
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.tiff"
with BarcodeReader(test_file, "image/tiff") as reader:
reader.detect()
separator_page_numbers = reader.get_separation_pages()
self.assertDictEqual(separator_page_numbers, {1: False})
@override_settings(
CONSUMER_BARCODE_TIFF_SUPPORT=True,
)
def test_scan_tiff_with_alpha_for_separating_barcodes(self):
"""
GIVEN:
- TIFF image containing barcodes
WHEN:
- Consume task returns
THEN:
- The file was split
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle-alpha.tiff"
with BarcodeReader(test_file, "image/tiff") as reader:
reader.detect()
separator_page_numbers = reader.get_separation_pages()
self.assertDictEqual(separator_page_numbers, {1: False})
def test_scan_file_for_separating_barcodes_none_present(self):
"""
GIVEN:
@ -285,6 +323,28 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertGreater(len(reader.barcodes), 0)
self.assertDictEqual(separator_page_numbers, {1: False})
def test_scan_file_for_separating_barcodes_password(self):
"""
GIVEN:
- Password protected PDF
WHEN:
- File is scanned for barcode
THEN:
- Scanning handles the exception without crashing
"""
test_file = self.SAMPLE_DIR / "password-is-test.pdf"
with self.assertLogs("paperless.barcodes", level="WARNING") as cm:
with BarcodeReader(test_file, "application/pdf") as reader:
reader.detect()
warning = cm.output[0]
expected_str = "WARNING:paperless.barcodes:File is likely password protected, not checking for barcodes"
self.assertTrue(warning.startswith(expected_str))
separator_page_numbers = reader.get_separation_pages()
self.assertEqual(reader.pdf_file, test_file)
self.assertDictEqual(separator_page_numbers, {})
def test_separate_pages(self):
"""
GIVEN:
@ -332,8 +392,12 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
with self.assertLogs("paperless.barcodes", level="WARNING") as cm:
with BarcodeReader(test_file, "application/pdf") as reader:
success = reader.separate(DocumentSource.ApiUpload)
self.assertFalse(success)
self.assertFalse(
reader.separate(
DocumentSource.ApiUpload,
DocumentMetadataOverrides(),
),
)
self.assertEqual(
cm.output,
[
@ -341,215 +405,6 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
],
)
def test_save_to_dir_given_name(self):
"""
GIVEN:
- File to save to a directory
- There is a name override
WHEN:
- The file is saved
THEN:
- The file exists
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
with BarcodeReader(test_file, "application/pdf") as reader:
reader.separate(DocumentSource.ApiUpload, "newname.pdf")
self.assertEqual(reader.pdf_file, test_file)
target_file1 = settings.CONSUMPTION_DIR / "0_newname.pdf"
target_file2 = settings.CONSUMPTION_DIR / "1_newname.pdf"
self.assertIsFile(target_file1)
self.assertIsFile(target_file2)
def test_barcode_splitter_api_upload(self):
"""
GIVEN:
- Input file containing barcodes
WHEN:
- Input file is split on barcodes
THEN:
- Correct number of files produced
"""
sample_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
test_file = settings.SCRATCH_DIR / "patch-code-t-middle.pdf"
shutil.copy(sample_file, test_file)
with BarcodeReader(test_file, "application/pdf") as reader:
reader.separate(DocumentSource.ApiUpload)
self.assertEqual(reader.pdf_file, test_file)
target_file1 = (
settings.CONSUMPTION_DIR / "patch-code-t-middle_document_0.pdf"
)
target_file2 = (
settings.CONSUMPTION_DIR / "patch-code-t-middle_document_1.pdf"
)
self.assertIsFile(target_file1)
self.assertIsFile(target_file2)
def test_barcode_splitter_consume_dir(self):
"""
GIVEN:
- Input file containing barcodes
WHEN:
- Input file is split on barcodes
THEN:
- Correct number of files produced
"""
sample_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
test_file = settings.CONSUMPTION_DIR / "patch-code-t-middle.pdf"
shutil.copy(sample_file, test_file)
with BarcodeReader(test_file, "application/pdf") as reader:
reader.detect()
reader.separate(DocumentSource.ConsumeFolder)
self.assertEqual(reader.pdf_file, test_file)
target_file1 = (
settings.CONSUMPTION_DIR / "patch-code-t-middle_document_0.pdf"
)
target_file2 = (
settings.CONSUMPTION_DIR / "patch-code-t-middle_document_1.pdf"
)
self.assertIsFile(target_file1)
self.assertIsFile(target_file2)
def test_barcode_splitter_consume_dir_recursive(self):
"""
GIVEN:
- Input file containing barcodes
- Input file is within a directory structure of the consume folder
WHEN:
- Input file is split on barcodes
THEN:
- Correct number of files produced
- Output files are within the same directory structure
"""
sample_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
test_file = (
settings.CONSUMPTION_DIR / "tag1" / "tag2" / "patch-code-t-middle.pdf"
)
test_file.parent.mkdir(parents=True)
shutil.copy(sample_file, test_file)
with BarcodeReader(test_file, "application/pdf") as reader:
reader.separate(DocumentSource.ConsumeFolder)
self.assertEqual(reader.pdf_file, test_file)
target_file1 = (
settings.CONSUMPTION_DIR
/ "tag1"
/ "tag2"
/ "patch-code-t-middle_document_0.pdf"
)
target_file2 = (
settings.CONSUMPTION_DIR
/ "tag1"
/ "tag2"
/ "patch-code-t-middle_document_1.pdf"
)
self.assertIsFile(target_file1)
self.assertIsFile(target_file2)
@override_settings(CONSUMER_ENABLE_BARCODES=True)
def test_consume_barcode_file(self):
"""
GIVEN:
- Input file with barcodes given to consume task
WHEN:
- Consume task returns
THEN:
- The file was split
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
dst = settings.SCRATCH_DIR / "patch-code-t-middle.pdf"
shutil.copy(test_file, dst)
with mock.patch("documents.tasks.async_to_sync"):
self.assertEqual(
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=dst,
),
None,
),
"File successfully split",
)
@override_settings(
CONSUMER_ENABLE_BARCODES=True,
CONSUMER_BARCODE_TIFF_SUPPORT=True,
)
def test_consume_barcode_tiff_file(self):
"""
GIVEN:
- TIFF image containing barcodes
WHEN:
- Consume task returns
THEN:
- The file was split
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.tiff"
dst = settings.SCRATCH_DIR / "patch-code-t-middle.tiff"
shutil.copy(test_file, dst)
with mock.patch("documents.tasks.async_to_sync"):
self.assertEqual(
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=dst,
),
None,
),
"File successfully split",
)
self.assertIsNotFile(dst)
@override_settings(
CONSUMER_ENABLE_BARCODES=True,
CONSUMER_BARCODE_TIFF_SUPPORT=True,
)
def test_consume_barcode_tiff_file_with_alpha(self):
"""
GIVEN:
- TIFF image containing barcodes
- TIFF image has an alpha layer
WHEN:
- Consume task handles the alpha layer and returns
THEN:
- The file was split without issue
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle-alpha.tiff"
dst = settings.SCRATCH_DIR / "patch-code-t-middle.tiff"
shutil.copy(test_file, dst)
with mock.patch("documents.tasks.async_to_sync"):
self.assertEqual(
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=dst,
),
None,
),
"File successfully split",
)
self.assertIsNotFile(dst)
@override_settings(
CONSUMER_ENABLE_BARCODES=True,
CONSUMER_BARCODE_TIFF_SUPPORT=True,
@ -597,60 +452,6 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertIsNone(kwargs["override_document_type_id"])
self.assertIsNone(kwargs["override_tag_ids"])
@override_settings(
CONSUMER_ENABLE_BARCODES=True,
CONSUMER_BARCODE_TIFF_SUPPORT=True,
)
def test_consume_barcode_supported_no_extension_file(self):
"""
GIVEN:
- TIFF image containing barcodes
- TIFF file is given without extension
WHEN:
- Consume task returns
THEN:
- The file was split
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.tiff"
dst = settings.SCRATCH_DIR / "patch-code-t-middle"
shutil.copy(test_file, dst)
with mock.patch("documents.tasks.async_to_sync"):
self.assertEqual(
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=dst,
),
None,
),
"File successfully split",
)
self.assertIsNotFile(dst)
def test_scan_file_for_separating_barcodes_password(self):
"""
GIVEN:
- Password protected PDF
WHEN:
- File is scanned for barcode
THEN:
- Scanning handles the exception without crashing
"""
test_file = self.SAMPLE_DIR / "password-is-test.pdf"
with self.assertLogs("paperless.barcodes", level="WARNING") as cm:
with BarcodeReader(test_file, "application/pdf") as reader:
reader.detect()
warning = cm.output[0]
expected_str = "WARNING:paperless.barcodes:File is likely password protected, not checking for barcodes"
self.assertTrue(warning.startswith(expected_str))
separator_page_numbers = reader.get_separation_pages()
self.assertEqual(reader.pdf_file, test_file)
self.assertDictEqual(separator_page_numbers, {})
@override_settings(
CONSUMER_ENABLE_BARCODES=True,
CONSUMER_ENABLE_ASN_BARCODE=True,
@ -722,11 +523,64 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertEqual(len(document_list), 5)
class TestAsnBarcode(DirectoriesMixin, TestCase):
SAMPLE_DIR = Path(__file__).parent / "samples"
@override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR")
class TestBarcodeNewConsume(
DirectoriesMixin,
FileSystemAssertsMixin,
SampleDirMixin,
DocumentConsumeDelayMixin,
TestCase,
):
@override_settings(CONSUMER_ENABLE_BARCODES=True)
def test_consume_barcode_file(self):
"""
GIVEN:
- Incoming file with at 1 barcode producing 2 documents
- Document includes metadata override information
WHEN:
- The document is split
THEN:
- Two new consume tasks are created
- Metadata overrides are preserved for the new consume
- The document source is unchanged (for consume templates)
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
temp_copy = self.dirs.scratch_dir / test_file.name
shutil.copy(test_file, temp_copy)
BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes"
overrides = DocumentMetadataOverrides(tag_ids=[1, 2, 9])
with mock.patch("documents.tasks.async_to_sync") as progress_mocker:
self.assertEqual(
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=temp_copy,
),
overrides,
),
"File successfully split",
)
# We let the consumer know progress is done
progress_mocker.assert_called_once()
# 2 new document consume tasks created
self.assertEqual(self.consume_file_mock.call_count, 2)
self.assertIsNotFile(temp_copy)
# Check the split files exist
# Check the source is unchanged
# Check the overrides are unchanged
for (
new_input_doc,
new_doc_overrides,
) in self.get_all_consume_delay_call_args():
self.assertEqual(new_input_doc.source, DocumentSource.ConsumeFolder)
self.assertIsFile(new_input_doc.original_file)
self.assertEqual(overrides, new_doc_overrides)
class TestAsnBarcode(DirectoriesMixin, SampleDirMixin, TestCase):
@override_settings(CONSUMER_ASN_BARCODE_PREFIX="CUSTOM-PREFIX-")
def test_scan_file_for_asn_custom_prefix(self):
"""

View File

@ -646,10 +646,13 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
with paperless_environment():
self.assertEqual(Document.objects.count(), 4)
self.assertEqual(CustomFieldInstance.objects.count(), 1)
Document.objects.all().delete()
CustomFieldInstance.objects.all().delete()
self.assertEqual(Document.objects.count(), 0)
call_command("document_importer", "--no-progress-bar", self.target)
self.assertEqual(Document.objects.count(), 4)
self.assertEqual(CustomFieldInstance.objects.count(), 1)
def test_folder_prefix(self):
"""

View File

@ -235,8 +235,10 @@ class DocumentConsumeDelayMixin:
"""
Iterates over all calls to the async task and returns the arguments
"""
# Must be at least 1 call
self.consume_file_mock.assert_called()
for args, _ in self.consume_file_mock.call_args_list:
for args, kwargs in self.consume_file_mock.call_args_list:
input_doc, overrides = args
yield (input_doc, overrides)
@ -244,7 +246,7 @@ class DocumentConsumeDelayMixin:
def get_specific_consume_delay_call_args(
self,
index: int,
) -> Iterator[tuple[ConsumableDocument, DocumentMetadataOverrides]]:
) -> tuple[ConsumableDocument, DocumentMetadataOverrides]:
"""
Returns the arguments of a specific call to the async task
"""
@ -299,3 +301,9 @@ class TestMigrations(TransactionTestCase):
def setUpBeforeMigration(self, apps):
pass
class SampleDirMixin:
SAMPLE_DIR = Path(__file__).parent / "samples"
BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes"

View File

@ -182,10 +182,14 @@ class PassUserMixin(CreateModelMixin):
class CorrespondentViewSet(ModelViewSet, PassUserMixin):
model = Correspondent
queryset = Correspondent.objects.annotate(
document_count=Count("documents"),
last_correspondence=Max("documents__created"),
).order_by(Lower("name"))
queryset = (
Correspondent.objects.annotate(
document_count=Count("documents"),
last_correspondence=Max("documents__created"),
)
.select_related("owner")
.order_by(Lower("name"))
)
serializer_class = CorrespondentSerializer
pagination_class = StandardPagination
@ -208,8 +212,12 @@ class CorrespondentViewSet(ModelViewSet, PassUserMixin):
class TagViewSet(ModelViewSet, PassUserMixin):
model = Tag
queryset = Tag.objects.annotate(document_count=Count("documents")).order_by(
Lower("name"),
queryset = (
Tag.objects.annotate(document_count=Count("documents"))
.select_related("owner")
.order_by(
Lower("name"),
)
)
def get_serializer_class(self, *args, **kwargs):
@ -232,9 +240,13 @@ class TagViewSet(ModelViewSet, PassUserMixin):
class DocumentTypeViewSet(ModelViewSet, PassUserMixin):
model = DocumentType
queryset = DocumentType.objects.annotate(
document_count=Count("documents"),
).order_by(Lower("name"))
queryset = (
DocumentType.objects.annotate(
document_count=Count("documents"),
)
.select_related("owner")
.order_by(Lower("name"))
)
serializer_class = DocumentTypeSerializer
pagination_class = StandardPagination
@ -283,7 +295,12 @@ class DocumentViewSet(
)
def get_queryset(self):
return Document.objects.distinct().annotate(num_notes=Count("notes"))
return (
Document.objects.distinct()
.annotate(num_notes=Count("notes"))
.select_related("correspondent", "storage_path", "document_type", "owner")
.prefetch_related("tags", "custom_fields", "notes")
)
def get_serializer(self, *args, **kwargs):
fields_param = self.request.query_params.get("fields", None)
@ -627,9 +644,18 @@ class DocumentViewSet(
class SearchResultSerializer(DocumentSerializer, PassUserMixin):
def to_representation(self, instance):
doc = Document.objects.get(id=instance["id"])
doc = (
Document.objects.select_related(
"correspondent",
"storage_path",
"document_type",
"owner",
)
.prefetch_related("tags", "custom_fields", "notes")
.get(id=instance["id"])
)
notes = ",".join(
[str(c.note) for c in Note.objects.filter(document=instance["id"])],
[str(c.note) for c in doc.notes.all()],
)
r = super().to_representation(doc)
r["__search_hit__"] = {
@ -752,7 +778,11 @@ class SavedViewViewSet(ModelViewSet, PassUserMixin):
def get_queryset(self):
user = self.request.user
return SavedView.objects.filter(owner=user)
return (
SavedView.objects.filter(owner=user)
.select_related("owner")
.prefetch_related("filter_rules")
)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
@ -1080,8 +1110,12 @@ class BulkDownloadView(GenericAPIView):
class StoragePathViewSet(ModelViewSet, PassUserMixin):
model = StoragePath
queryset = StoragePath.objects.annotate(document_count=Count("documents")).order_by(
Lower("name"),
queryset = (
StoragePath.objects.annotate(document_count=Count("documents"))
.select_related("owner")
.order_by(
Lower("name"),
)
)
serializer_class = StoragePathSerializer
@ -1347,7 +1381,18 @@ class ConsumptionTemplateViewSet(ModelViewSet):
model = ConsumptionTemplate
queryset = ConsumptionTemplate.objects.all().order_by("order")
queryset = (
ConsumptionTemplate.objects.prefetch_related(
"assign_tags",
"assign_view_users",
"assign_view_groups",
"assign_change_users",
"assign_change_groups",
"assign_custom_fields",
)
.all()
.order_by("order")
)
class CustomFieldViewSet(ModelViewSet):

View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-05 08:26-0800\n"
"PO-Revision-Date: 2023-12-12 00:24\n"
"PO-Revision-Date: 2023-12-14 00:23\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"

View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-05 08:26-0800\n"
"PO-Revision-Date: 2023-12-05 16:27\n"
"PO-Revision-Date: 2023-12-13 12:09\n"
"Last-Translator: \n"
"Language-Team: Croatian\n"
"Language: hr_HR\n"

View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-05 08:26-0800\n"
"PO-Revision-Date: 2023-12-05 16:27\n"
"PO-Revision-Date: 2023-12-16 00:23\n"
"Last-Translator: \n"
"Language-Team: Romanian\n"
"Language: ro_RO\n"
@ -23,11 +23,11 @@ msgstr "Documente"
#: documents/models.py:36 documents/models.py:734
msgid "owner"
msgstr ""
msgstr "proprietar"
#: documents/models.py:53
msgid "None"
msgstr ""
msgstr "Nimic"
#: documents/models.py:54
msgid "Any word"
@ -108,15 +108,15 @@ msgstr "tipuri de document"
#: documents/models.py:124
msgid "path"
msgstr ""
msgstr "cale"
#: documents/models.py:129 documents/models.py:156
msgid "storage path"
msgstr ""
msgstr "cale de stocare"
#: documents/models.py:130
msgid "storage paths"
msgstr ""
msgstr "căi de stocare"
#: documents/models.py:137
msgid "Unencrypted"
@ -193,11 +193,11 @@ msgstr "Numele curent al arhivei stocate"
#: documents/models.py:250
msgid "original filename"
msgstr ""
msgstr "numele original al fișierului"
#: documents/models.py:256
msgid "The original name of the file when it was uploaded"
msgstr ""
msgstr "Numele original al fișierului când a fost încărcat"
#: documents/models.py:263
msgid "archive serial number"
@ -381,47 +381,47 @@ msgstr ""
#: documents/models.py:447
msgid "storage path is"
msgstr ""
msgstr "calea de stocare este"
#: documents/models.py:448
msgid "has correspondent in"
msgstr ""
msgstr "are corespondent în"
#: documents/models.py:449
msgid "does not have correspondent in"
msgstr ""
msgstr "nu are corespondent în"
#: documents/models.py:450
msgid "has document type in"
msgstr ""
msgstr "are tip de document în"
#: documents/models.py:451
msgid "does not have document type in"
msgstr ""
msgstr "nu are tip document în"
#: documents/models.py:452
msgid "has storage path in"
msgstr ""
msgstr "are cale de stocare în"
#: documents/models.py:453
msgid "does not have storage path in"
msgstr ""
msgstr "nu are cale de stocare în"
#: documents/models.py:454
msgid "owner is"
msgstr ""
msgstr "proprietarul este"
#: documents/models.py:455
msgid "has owner in"
msgstr ""
msgstr "are proprietar în"
#: documents/models.py:456
msgid "does not have owner"
msgstr ""
msgstr "nu are proprietar"
#: documents/models.py:457
msgid "does not have owner in"
msgstr ""
msgstr "nu are proprietar în"
#: documents/models.py:467
msgid "rule type"
@ -441,47 +441,47 @@ msgstr "reguli de filtrare"
#: documents/models.py:584
msgid "Task ID"
msgstr ""
msgstr "ID Sarcină"
#: documents/models.py:585
msgid "Celery ID for the Task that was run"
msgstr ""
msgstr "ID-ul sarcinii Celery care a fost rulată"
#: documents/models.py:590
msgid "Acknowledged"
msgstr ""
msgstr "Confirmat"
#: documents/models.py:591
msgid "If the task is acknowledged via the frontend or API"
msgstr ""
msgstr "Dacă sarcina este confirmată prin frontend sau API"
#: documents/models.py:597
msgid "Task Filename"
msgstr ""
msgstr "Numele fișierului sarcină"
#: documents/models.py:598
msgid "Name of the file which the Task was run for"
msgstr ""
msgstr "Numele fișierului pentru care sarcina a fost executată"
#: documents/models.py:604
msgid "Task Name"
msgstr ""
msgstr "Nume sarcină"
#: documents/models.py:605
msgid "Name of the Task which was run"
msgstr ""
msgstr "Numele sarcinii care a fost executată"
#: documents/models.py:612
msgid "Task State"
msgstr ""
msgstr "Stare sarcină"
#: documents/models.py:613
msgid "Current state of the task being run"
msgstr ""
msgstr "Stadiul actual al sarcinii în curs de desfășurare"
#: documents/models.py:618
msgid "Created DateTime"
msgstr ""
msgstr "Data creării"
#: documents/models.py:619
msgid "Datetime field when the task result was created in UTC"
@ -489,7 +489,7 @@ msgstr ""
#: documents/models.py:624
msgid "Started DateTime"
msgstr ""
msgstr "Data începerii"
#: documents/models.py:625
msgid "Datetime field when the task was started in UTC"
@ -497,7 +497,7 @@ msgstr ""
#: documents/models.py:630
msgid "Completed DateTime"
msgstr ""
msgstr "Data finalizării"
#: documents/models.py:631
msgid "Datetime field when the task was completed in UTC"
@ -505,15 +505,15 @@ msgstr ""
#: documents/models.py:636
msgid "Result Data"
msgstr ""
msgstr "Datele rezultatului"
#: documents/models.py:638
msgid "The data returned by the task"
msgstr ""
msgstr "Datele returnate de sarcină"
#: documents/models.py:650
msgid "Note for the document"
msgstr ""
msgstr "Notă pentru document"
#: documents/models.py:674
msgid "user"
@ -521,23 +521,23 @@ msgstr "utilizator"
#: documents/models.py:679
msgid "note"
msgstr ""
msgstr "notă"
#: documents/models.py:680
msgid "notes"
msgstr ""
msgstr "note"
#: documents/models.py:688
msgid "Archive"
msgstr ""
msgstr "Arhivă"
#: documents/models.py:689
msgid "Original"
msgstr ""
msgstr "Original"
#: documents/models.py:700
msgid "expiration"
msgstr ""
msgstr "expirare"
#: documents/models.py:707
msgid "slug"
@ -545,35 +545,35 @@ msgstr ""
#: documents/models.py:739
msgid "share link"
msgstr ""
msgstr "link de partajare"
#: documents/models.py:740
msgid "share links"
msgstr ""
msgstr "link-uri de partajare"
#: documents/models.py:752
msgid "String"
msgstr ""
msgstr "Şir de caractere"
#: documents/models.py:753
msgid "URL"
msgstr ""
msgstr "Adresă URL"
#: documents/models.py:754
msgid "Date"
msgstr ""
msgstr "Dată"
#: documents/models.py:755
msgid "Boolean"
msgstr ""
msgstr "Boolean"
#: documents/models.py:756
msgid "Integer"
msgstr ""
msgstr "Număr întreg"
#: documents/models.py:757
msgid "Float"
msgstr ""
msgstr "Număr zecimal"
#: documents/models.py:758
msgid "Monetary"
@ -581,11 +581,11 @@ msgstr ""
#: documents/models.py:759
msgid "Document Link"
msgstr ""
msgstr "Link document"
#: documents/models.py:771
msgid "data type"
msgstr ""
msgstr "tip date"
#: documents/models.py:779
msgid "custom field"

View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-05 08:26-0800\n"
"PO-Revision-Date: 2023-12-05 16:27\n"
"PO-Revision-Date: 2023-12-14 00:23\n"
"Last-Translator: \n"
"Language-Team: Chinese Traditional\n"
"Language: zh_TW\n"
@ -47,7 +47,7 @@ msgstr ""
#: documents/models.py:58
msgid "Fuzzy word"
msgstr ""
msgstr "模糊詞"
#: documents/models.py:59
msgid "Automatic"
@ -68,15 +68,15 @@ msgstr "比對演算法"
#: documents/models.py:72
msgid "is insensitive"
msgstr ""
msgstr "不區分大小寫"
#: documents/models.py:95 documents/models.py:147
msgid "correspondent"
msgstr ""
msgstr "聯繫者"
#: documents/models.py:96
msgid "correspondents"
msgstr ""
msgstr "聯繫者"
#: documents/models.py:100
msgid "color"
@ -84,47 +84,47 @@ msgstr "顏色"
#: documents/models.py:103
msgid "is inbox tag"
msgstr ""
msgstr "收件匣標籤"
#: documents/models.py:106
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr ""
msgstr "標記此標籤為收件匣標籤:所有新處理的文件將會以此收件匣標籤作標記。"
#: documents/models.py:112
msgid "tag"
msgstr ""
msgstr "標籤"
#: documents/models.py:113 documents/models.py:185
msgid "tags"
msgstr ""
msgstr "標籤"
#: documents/models.py:118 documents/models.py:167
msgid "document type"
msgstr ""
msgstr "文件類型"
#: documents/models.py:119
msgid "document types"
msgstr ""
msgstr "文件類型"
#: documents/models.py:124
msgid "path"
msgstr ""
msgstr "位址"
#: documents/models.py:129 documents/models.py:156
msgid "storage path"
msgstr ""
msgstr "儲存位址"
#: documents/models.py:130
msgid "storage paths"
msgstr ""
msgstr "儲存位址"
#: documents/models.py:137
msgid "Unencrypted"
msgstr ""
msgstr "未加密"
#: documents/models.py:138
msgid "Encrypted with GNU Privacy Guard"
msgstr ""
msgstr "已使用 GNU Privacy Guard 進行加密"
#: documents/models.py:159
msgid "title"
@ -189,27 +189,27 @@ msgstr "存檔檔案名稱"
#: documents/models.py:246
msgid "Current archive filename in storage"
msgstr ""
msgstr "現時儲存空間封存的檔案名稱"
#: documents/models.py:250
msgid "original filename"
msgstr ""
msgstr "原先檔案名稱"
#: documents/models.py:256
msgid "The original name of the file when it was uploaded"
msgstr ""
msgstr "檔案上傳時的檔案名稱"
#: documents/models.py:263
msgid "archive serial number"
msgstr ""
msgstr "封存編號"
#: documents/models.py:273
msgid "The position of this document in your physical document archive."
msgstr ""
msgstr "此檔案在你實體儲存空間的位置。"
#: documents/models.py:279 documents/models.py:665 documents/models.py:719
msgid "document"
msgstr ""
msgstr "文件"
#: documents/models.py:280
msgid "documents"
@ -217,47 +217,47 @@ msgstr "文件"
#: documents/models.py:368
msgid "debug"
msgstr ""
msgstr "偵錯"
#: documents/models.py:369
msgid "information"
msgstr ""
msgstr "資訊"
#: documents/models.py:370
msgid "warning"
msgstr ""
msgstr "警告"
#: documents/models.py:371 paperless_mail/models.py:305
msgid "error"
msgstr ""
msgstr "錯誤"
#: documents/models.py:372
msgid "critical"
msgstr ""
msgstr "嚴重"
#: documents/models.py:375
msgid "group"
msgstr ""
msgstr "群組"
#: documents/models.py:377
msgid "message"
msgstr ""
msgstr "訊息"
#: documents/models.py:380
msgid "level"
msgstr ""
msgstr "程度"
#: documents/models.py:389
msgid "log"
msgstr ""
msgstr "記錄"
#: documents/models.py:390
msgid "logs"
msgstr ""
msgstr "記錄"
#: documents/models.py:399 documents/models.py:464
msgid "saved view"
msgstr ""
msgstr "已儲存的檢視表"
#: documents/models.py:400
msgid "saved views"
@ -265,207 +265,207 @@ msgstr "保存視圖"
#: documents/models.py:405
msgid "show on dashboard"
msgstr ""
msgstr "顯示在概覽"
#: documents/models.py:408
msgid "show in sidebar"
msgstr ""
msgstr "顯示在側邊欄"
#: documents/models.py:412
msgid "sort field"
msgstr ""
msgstr "排序欄位"
#: documents/models.py:417
msgid "sort reverse"
msgstr ""
msgstr "倒轉排序"
#: documents/models.py:422
msgid "title contains"
msgstr ""
msgstr "標題包含"
#: documents/models.py:423
msgid "content contains"
msgstr ""
msgstr "內容包含"
#: documents/models.py:424
msgid "ASN is"
msgstr ""
msgstr "ASN 為"
#: documents/models.py:425
msgid "correspondent is"
msgstr ""
msgstr "聯繫者為"
#: documents/models.py:426
msgid "document type is"
msgstr ""
msgstr "文件類型為"
#: documents/models.py:427
msgid "is in inbox"
msgstr ""
msgstr "在收件匣內"
#: documents/models.py:428
msgid "has tag"
msgstr ""
msgstr "包含標籤"
#: documents/models.py:429
msgid "has any tag"
msgstr ""
msgstr "包含任何標籤"
#: documents/models.py:430
msgid "created before"
msgstr ""
msgstr "建立時間之前"
#: documents/models.py:431
msgid "created after"
msgstr ""
msgstr "建立時間之後"
#: documents/models.py:432
msgid "created year is"
msgstr ""
msgstr "建立年份為"
#: documents/models.py:433
msgid "created month is"
msgstr ""
msgstr "建立月份為"
#: documents/models.py:434
msgid "created day is"
msgstr ""
msgstr "建立日期為"
#: documents/models.py:435
msgid "added before"
msgstr ""
msgstr "加入時間之前"
#: documents/models.py:436
msgid "added after"
msgstr ""
msgstr "加入時間之後"
#: documents/models.py:437
msgid "modified before"
msgstr ""
msgstr "修改之前"
#: documents/models.py:438
msgid "modified after"
msgstr ""
msgstr "修改之後"
#: documents/models.py:439
msgid "does not have tag"
msgstr ""
msgstr "沒有包含標籤"
#: documents/models.py:440
msgid "does not have ASN"
msgstr ""
msgstr "沒有包含 ASN"
#: documents/models.py:441
msgid "title or content contains"
msgstr ""
msgstr "標題或內容包含"
#: documents/models.py:442
msgid "fulltext query"
msgstr ""
msgstr "全文搜索"
#: documents/models.py:443
msgid "more like this"
msgstr ""
msgstr "其他類似內容"
#: documents/models.py:444
msgid "has tags in"
msgstr ""
msgstr "含有這個標籤"
#: documents/models.py:445
msgid "ASN greater than"
msgstr ""
msgstr "ASN 大於"
#: documents/models.py:446
msgid "ASN less than"
msgstr ""
msgstr "ASN 小於"
#: documents/models.py:447
msgid "storage path is"
msgstr ""
msgstr "儲存位址為"
#: documents/models.py:448
msgid "has correspondent in"
msgstr ""
msgstr "包含聯繫者"
#: documents/models.py:449
msgid "does not have correspondent in"
msgstr ""
msgstr "沒有包含聯繫者"
#: documents/models.py:450
msgid "has document type in"
msgstr ""
msgstr "文件類型包含"
#: documents/models.py:451
msgid "does not have document type in"
msgstr ""
msgstr "沒有包含的文件類型"
#: documents/models.py:452
msgid "has storage path in"
msgstr ""
msgstr "儲存位址包含"
#: documents/models.py:453
msgid "does not have storage path in"
msgstr ""
msgstr "沒有包含的儲存位址"
#: documents/models.py:454
msgid "owner is"
msgstr ""
msgstr "擁有者為"
#: documents/models.py:455
msgid "has owner in"
msgstr ""
msgstr "擁有者包含"
#: documents/models.py:456
msgid "does not have owner"
msgstr ""
msgstr "沒有包含的擁有者"
#: documents/models.py:457
msgid "does not have owner in"
msgstr ""
msgstr "沒有包含的擁有者"
#: documents/models.py:467
msgid "rule type"
msgstr ""
msgstr "規則類型"
#: documents/models.py:469
msgid "value"
msgstr ""
msgstr "數值"
#: documents/models.py:472
msgid "filter rule"
msgstr ""
msgstr "過濾規則"
#: documents/models.py:473
msgid "filter rules"
msgstr ""
msgstr "過濾規則"
#: documents/models.py:584
msgid "Task ID"
msgstr ""
msgstr "任務 ID"
#: documents/models.py:585
msgid "Celery ID for the Task that was run"
msgstr ""
msgstr "已執行任務的 Celery ID"
#: documents/models.py:590
msgid "Acknowledged"
msgstr ""
msgstr "已確認"
#: documents/models.py:591
msgid "If the task is acknowledged via the frontend or API"
msgstr ""
msgstr "如果任務已由前端 / API 確認"
#: documents/models.py:597
msgid "Task Filename"
msgstr ""
msgstr "任務檔案名稱"
#: documents/models.py:598
msgid "Name of the file which the Task was run for"
msgstr ""
msgstr "執行任務的目標檔案名稱"
#: documents/models.py:604
msgid "Task Name"
msgstr ""
msgstr "任務名稱"
#: documents/models.py:605
msgid "Name of the Task which was run"
@ -473,7 +473,7 @@ msgstr ""
#: documents/models.py:612
msgid "Task State"
msgstr ""
msgstr "任務狀態"
#: documents/models.py:613
msgid "Current state of the task being run"
@ -657,7 +657,7 @@ msgstr ""
#: documents/models.py:967 paperless_mail/models.py:238
msgid "assign this correspondent"
msgstr ""
msgstr "指派這個聯繫者"
#: documents/models.py:975
msgid "assign this storage path"
@ -1128,7 +1128,7 @@ msgstr ""
#: paperless_mail/models.py:88
msgid "Do not assign a correspondent"
msgstr ""
msgstr "不要指派聯繫者"
#: paperless_mail/models.py:89
msgid "Use mail address"
@ -1140,7 +1140,7 @@ msgstr ""
#: paperless_mail/models.py:91
msgid "Use correspondent selected below"
msgstr ""
msgstr "使用以下已選擇的聯繫者"
#: paperless_mail/models.py:101
msgid "account"
@ -1220,7 +1220,7 @@ msgstr ""
#: paperless_mail/models.py:228
msgid "assign correspondent from"
msgstr ""
msgstr "指派聯繫者從"
#: paperless_mail/models.py:242
msgid "Assign the rule owner to documents"

View File

@ -254,7 +254,7 @@ class RasterisedDocumentParser(DocumentParser):
f"Image DPI of {ocrmypdf_args['image_dpi']} is low, OCR may fail",
)
if settings.OCR_USER_ARGS and not safe_fallback:
if settings.OCR_USER_ARGS:
try:
user_args = json.loads(settings.OCR_USER_ARGS)
ocrmypdf_args = {**ocrmypdf_args, **user_args}