Merge branch 'dev' into fix-mail-starttls

This commit is contained in:
phail
2022-04-13 23:55:38 +02:00
128 changed files with 11610 additions and 6203 deletions

View File

@@ -3,7 +3,9 @@ import os
from pathlib import Path
from pathlib import PurePath
from threading import Thread
from time import monotonic
from time import sleep
from typing import Final
from django.conf import settings
from django.core.management.base import BaseCommand
@@ -53,6 +55,25 @@ def _consume(filepath):
logger.warning(f"Not consuming file {filepath}: Unknown file extension.")
return
# Total wait time: up to 500ms
os_error_retry_count: Final[int] = 50
os_error_retry_wait: Final[float] = 0.01
read_try_count = 0
file_open_ok = False
while (read_try_count < os_error_retry_count) and not file_open_ok:
try:
with open(filepath, "rb"):
file_open_ok = True
except OSError:
read_try_count += 1
sleep(os_error_retry_wait)
if read_try_count >= os_error_retry_count:
logger.warning(f"Not consuming file {filepath}: OS reports file as busy still")
return
tag_ids = None
try:
if settings.CONSUMER_SUBDIRS_AS_TAGS:
@@ -81,19 +102,23 @@ def _consume_wait_unmodified(file):
logger.debug(f"Waiting for file {file} to remain unmodified")
mtime = -1
size = -1
current_try = 0
while current_try < settings.CONSUMER_POLLING_RETRY_COUNT:
try:
new_mtime = os.stat(file).st_mtime
stat_data = os.stat(file)
new_mtime = stat_data.st_mtime
new_size = stat_data.st_size
except FileNotFoundError:
logger.debug(
f"File {file} moved while waiting for it to remain " f"unmodified.",
)
return
if new_mtime == mtime:
if new_mtime == mtime and new_size == size:
_consume(file)
return
mtime = new_mtime
size = new_size
sleep(settings.CONSUMER_POLLING_DELAY)
current_try += 1
@@ -182,14 +207,32 @@ class Command(BaseCommand):
descriptor = inotify.add_watch(directory, inotify_flags)
try:
inotify_debounce: Final[float] = 0.5
notified_files = {}
while not self.stop_flag:
for event in inotify.read(timeout=1000):
if recursive:
path = inotify.get_path(event.wd)
else:
path = directory
filepath = os.path.join(path, event.name)
_consume(filepath)
notified_files[filepath] = monotonic()
# Check the files against the timeout
still_waiting = {}
for filepath in notified_files:
# Time of the last inotify event for this file
last_event_time = notified_files[filepath]
if (monotonic() - last_event_time) > inotify_debounce:
_consume(filepath)
else:
still_waiting[filepath] = last_event_time
# These files are still waiting to hit the timeout
notified_files = still_waiting
except KeyboardInterrupt:
pass

View File

@@ -60,7 +60,7 @@ def match_tags(document, classifier):
def matches(matching_model, document):
search_kwargs = {}
document_content = document.content.lower()
document_content = document.content
# Check that match is not empty
if matching_model.match.strip() == "":

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.0.3 on 2022-04-01 22:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("documents", "1017_alter_savedviewfilterrule_rule_type"),
]
operations = [
migrations.AlterField(
model_name="savedviewfilterrule",
name="value",
field=models.CharField(
blank=True, max_length=255, null=True, verbose_name="value"
),
),
]

View File

@@ -375,7 +375,7 @@ class SavedViewFilterRule(models.Model):
rule_type = models.PositiveIntegerField(_("rule type"), choices=RULE_TYPES)
value = models.CharField(_("value"), max_length=128, blank=True, null=True)
value = models.CharField(_("value"), max_length=255, blank=True, null=True)
class Meta:
verbose_name = _("filter rule")

View File

@@ -23,6 +23,7 @@ from documents.signals import document_consumer_declaration
# - XX. MONTH ZZZZ with XX being 1 or 2 and ZZZZ being 2 or 4 digits
# - MONTH ZZZZ, with ZZZZ being 4 digits
# - MONTH XX, ZZZZ with XX being 1 or 2 and ZZZZ being 4 digits
# - XX MON ZZZZ with XX being 1 or 2 and ZZZZ being 4 digits. MONTH is 3 letters
# TODO: isnt there a date parsing library for this?
@@ -31,7 +32,8 @@ DATE_REGEX = re.compile(
r"(\b|(?!=([_-])))([0-9]{4}|[0-9]{2})[\.\/-]([0-9]{1,2})[\.\/-]([0-9]{1,2})(\b|(?=([_-])))|" # noqa: E501
r"(\b|(?!=([_-])))([0-9]{1,2}[\. ]+[^ ]{3,9} ([0-9]{4}|[0-9]{2}))(\b|(?=([_-])))|" # noqa: E501
r"(\b|(?!=([_-])))([^\W\d_]{3,9} [0-9]{1,2}, ([0-9]{4}))(\b|(?=([_-])))|"
r"(\b|(?!=([_-])))([^\W\d_]{3,9} [0-9]{4})(\b|(?=([_-])))",
r"(\b|(?!=([_-])))([^\W\d_]{3,9} [0-9]{4})(\b|(?=([_-])))|"
r"(\b|(?!=([_-])))(\b[0-9]{1,2}[ \.\/-][A-Z]{3}[ \.\/-][0-9]{4})(\b|(?=([_-])))", # noqa: E501
)

View File

@@ -1,5 +1,6 @@
import logging
import os
import shutil
from django.conf import settings
from django.contrib.admin.models import ADDITION
@@ -252,7 +253,7 @@ def cleanup_document_deletion(sender, instance, using, **kwargs):
logger.debug(f"Moving {instance.source_path} to trash at {new_file_path}")
try:
os.rename(instance.source_path, new_file_path)
shutil.move(instance.source_path, new_file_path)
except OSError as e:
logger.error(
f"Failed to move {instance.source_path} to trash at "

View File

@@ -1,6 +1,12 @@
import logging
import os
import shutil
import tempfile
from typing import List # for type hinting. Can be removed, if only Python >3.8 is used
import tqdm
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.conf import settings
from django.db.models.signals import post_save
from documents import index
@@ -14,8 +20,12 @@ from documents.models import Document
from documents.models import DocumentType
from documents.models import Tag
from documents.sanity_checker import SanityCheckFailedException
from pdf2image import convert_from_path
from pikepdf import Pdf
from pyzbar import pyzbar
from whoosh.writing import AsyncWriter
logger = logging.getLogger("paperless.tasks")
@@ -62,6 +72,115 @@ def train_classifier():
logger.warning("Classifier error: " + str(e))
def barcode_reader(image) -> List[str]:
"""
Read any barcodes contained in image
Returns a list containing all found barcodes
"""
barcodes = []
# Decode the barcode image
detected_barcodes = pyzbar.decode(image)
if detected_barcodes:
# Traverse through all the detected barcodes in image
for barcode in detected_barcodes:
if barcode.data:
decoded_barcode = barcode.data.decode("utf-8")
barcodes.append(decoded_barcode)
logger.debug(
f"Barcode of type {str(barcode.type)} found: {decoded_barcode}",
)
return barcodes
def scan_file_for_separating_barcodes(filepath: str) -> List[int]:
"""
Scan the provided file for page separating barcodes
Returns a list of pagenumbers, which separate the file
"""
separator_page_numbers = []
separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
# use a temporary directory in case the file os too big to handle in memory
with tempfile.TemporaryDirectory() as path:
pages_from_path = convert_from_path(filepath, output_folder=path)
for current_page_number, page in enumerate(pages_from_path):
current_barcodes = barcode_reader(page)
if separator_barcode in current_barcodes:
separator_page_numbers.append(current_page_number)
return separator_page_numbers
def separate_pages(filepath: str, pages_to_split_on: List[int]) -> List[str]:
"""
Separate the provided file on the pages_to_split_on.
The pages which are defined by page_numbers will be removed.
Returns a list of (temporary) filepaths to consume.
These will need to be deleted later.
"""
os.makedirs(settings.SCRATCH_DIR, exist_ok=True)
tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR)
fname = os.path.splitext(os.path.basename(filepath))[0]
pdf = Pdf.open(filepath)
document_paths = []
logger.debug(f"Temp dir is {str(tempdir)}")
if not pages_to_split_on:
logger.warning("No pages to split on!")
else:
# go from the first page to the first separator page
dst = Pdf.new()
for n, page in enumerate(pdf.pages):
if n < pages_to_split_on[0]:
dst.pages.append(page)
output_filename = "{}_document_0.pdf".format(fname)
savepath = os.path.join(tempdir, output_filename)
with open(savepath, "wb") as out:
dst.save(out)
document_paths = [savepath]
# iterate through the rest of the document
for count, page_number in enumerate(pages_to_split_on):
logger.debug(f"Count: {str(count)} page_number: {str(page_number)}")
dst = Pdf.new()
try:
next_page = pages_to_split_on[count + 1]
except IndexError:
next_page = len(pdf.pages)
# skip the first page_number. This contains the barcode page
for page in range(page_number + 1, next_page):
logger.debug(
f"page_number: {str(page_number)} next_page: {str(next_page)}",
)
dst.pages.append(pdf.pages[page])
output_filename = "{}_document_{}.pdf".format(fname, str(count + 1))
logger.debug(f"pdf no:{str(count)} has {str(len(dst.pages))} pages")
savepath = os.path.join(tempdir, output_filename)
with open(savepath, "wb") as out:
dst.save(out)
document_paths.append(savepath)
logger.debug(f"Temp files are {str(document_paths)}")
return document_paths
def save_to_dir(
filepath: str,
newname: str = None,
target_dir: str = settings.CONSUMPTION_DIR,
):
"""
Copies filepath to target_dir.
Optionally rename the file.
"""
if os.path.isfile(filepath) and os.path.isdir(target_dir):
dst = shutil.copy(filepath, target_dir)
logging.debug(f"saved {str(filepath)} to {str(dst)}")
if newname:
dst_new = os.path.join(target_dir, newname)
logger.debug(f"moving {str(dst)} to {str(dst_new)}")
os.rename(dst, dst_new)
else:
logger.warning(f"{str(filepath)} or {str(target_dir)} don't exist.")
def consume_file(
path,
override_filename=None,
@@ -72,6 +191,48 @@ def consume_file(
task_id=None,
):
# check for separators in current document
if settings.CONSUMER_ENABLE_BARCODES:
separators = []
document_list = []
separators = scan_file_for_separating_barcodes(path)
if separators:
logger.debug(f"Pages with separators found in: {str(path)}")
document_list = separate_pages(path, separators)
if document_list:
for n, document in enumerate(document_list):
# save to consumption dir
# rename it to the original filename with number prefix
if override_filename:
newname = f"{str(n)}_" + override_filename
else:
newname = None
save_to_dir(document, newname=newname)
# if we got here, the document was successfully split
# and can safely be deleted
logger.debug("Deleting file {}".format(path))
os.unlink(path)
# notify the sender, otherwise the progress bar
# in the UI stays stuck
payload = {
"filename": override_filename,
"task_id": task_id,
"current_progress": 100,
"max_progress": 100,
"status": "SUCCESS",
"message": "finished",
}
try:
async_to_sync(get_channel_layer().group_send)(
"status_updates",
{"type": "status_update", "data": payload},
)
except OSError as e:
logger.warning("OSError. It could be, the broker cannot be reached.")
logger.warning(str(e))
return "File successfully split"
# continue with consumption if no barcode was found
document = Consumer().try_consume_file(
path,
override_filename=override_filename,

View File

@@ -23,8 +23,10 @@
<script type="text/javascript">
setTimeout(() => {
let warning = document.getElementsByClassName('warning').item(0)
warning.classList.remove('hide')
warning.classList.add('show')
if (warning) {
warning.classList.remove('hide')
warning.classList.add('show')
}
}, 8000)
</script>
<style type="text/css">

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 B

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -100,6 +100,57 @@ class TestDate(TestCase):
datetime.datetime(2020, 3, 1, 0, 0, tzinfo=tz.gettz(settings.TIME_ZONE)),
)
def test_date_format_10(self):
text = "Customer Number Currency 22-MAR-2022 Credit Card 1934829304"
self.assertEqual(
parse_date("", text),
datetime.datetime(2022, 3, 22, 0, 0, tzinfo=tz.gettz(settings.TIME_ZONE)),
)
def test_date_format_11(self):
text = "Customer Number Currency 22 MAR 2022 Credit Card 1934829304"
self.assertEqual(
parse_date("", text),
datetime.datetime(2022, 3, 22, 0, 0, tzinfo=tz.gettz(settings.TIME_ZONE)),
)
def test_date_format_12(self):
text = "Customer Number Currency 22/MAR/2022 Credit Card 1934829304"
self.assertEqual(
parse_date("", text),
datetime.datetime(2022, 3, 22, 0, 0, tzinfo=tz.gettz(settings.TIME_ZONE)),
)
def test_date_format_13(self):
text = "Customer Number Currency 22.MAR.2022 Credit Card 1934829304"
self.assertEqual(
parse_date("", text),
datetime.datetime(2022, 3, 22, 0, 0, tzinfo=tz.gettz(settings.TIME_ZONE)),
)
def test_date_format_14(self):
text = "Customer Number Currency 22.MAR 2022 Credit Card 1934829304"
self.assertEqual(
parse_date("", text),
datetime.datetime(2022, 3, 22, 0, 0, tzinfo=tz.gettz(settings.TIME_ZONE)),
)
def test_date_format_15(self):
text = "Customer Number Currency 22.MAR.22 Credit Card 1934829304"
self.assertIsNone(parse_date("", text), None)
def test_date_format_16(self):
text = "Customer Number Currency 22.MAR,22 Credit Card 1934829304"
self.assertIsNone(parse_date("", text), None)
def test_date_format_17(self):
text = "Customer Number Currency 22,MAR,2022 Credit Card 1934829304"
self.assertIsNone(parse_date("", text), None)
def test_date_format_18(self):
text = "Customer Number Currency 22 MAR,2022 Credit Card 1934829304"
self.assertIsNone(parse_date("", text), None)
def test_crazy_date_past(self, *args):
self.assertIsNone(parse_date("", "01-07-0590 00:00:00"))

View File

@@ -260,6 +260,21 @@ class TestConsumer(DirectoriesMixin, ConsumerMixin, TransactionTestCase):
f'_is_ignored("{file_path}") != {expected_ignored}',
)
@mock.patch("documents.management.commands.document_consumer.open")
def test_consume_file_busy(self, open_mock):
# Calling this mock always raises this
open_mock.side_effect = OSError
self.t_start()
f = os.path.join(self.dirs.consumption_dir, "my_file.pdf")
shutil.copy(self.sample_file, f)
self.wait_for_task_mock_call()
self.task_mock.assert_not_called()
@override_settings(
CONSUMER_POLLING=1,

View File

@@ -1,6 +1,7 @@
import shutil
import tempfile
from random import randint
from typing import Iterable
from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import User
@@ -15,27 +16,37 @@ from ..models import Tag
from ..signals import document_consumption_finished
class TestMatching(TestCase):
def _test_matching(self, text, algorithm, true, false):
class _TestMatchingBase(TestCase):
def _test_matching(
self,
match_text: str,
match_algorithm: str,
should_match: Iterable[str],
no_match: Iterable[str],
case_sensitive: bool = False,
):
for klass in (Tag, Correspondent, DocumentType):
instance = klass.objects.create(
name=str(randint(10000, 99999)),
match=text,
matching_algorithm=getattr(klass, algorithm),
match=match_text,
matching_algorithm=getattr(klass, match_algorithm),
is_insensitive=not case_sensitive,
)
for string in true:
for string in should_match:
doc = Document(content=string)
self.assertTrue(
matching.matches(instance, doc),
'"%s" should match "%s" but it does not' % (text, string),
'"%s" should match "%s" but it does not' % (match_text, string),
)
for string in false:
for string in no_match:
doc = Document(content=string)
self.assertFalse(
matching.matches(instance, doc),
'"%s" should not match "%s" but it does' % (text, string),
'"%s" should not match "%s" but it does' % (match_text, string),
)
class TestMatching(_TestMatchingBase):
def test_match_all(self):
self._test_matching(
@@ -202,6 +213,169 @@ class TestMatching(TestCase):
)
class TestCaseSensitiveMatching(_TestMatchingBase):
def test_match_all(self):
self._test_matching(
"alpha charlie gamma",
"MATCH_ALL",
(
"I have alpha, charlie, and gamma in me",
"I have gamma, charlie, and alpha in me",
),
(
"I have Alpha, charlie, and gamma in me",
"I have gamma, Charlie, and alpha in me",
"I have alpha, charlie, and Gamma in me",
"I have gamma, charlie, and ALPHA in me",
),
case_sensitive=True,
)
self._test_matching(
"Alpha charlie Gamma",
"MATCH_ALL",
(
"I have Alpha, charlie, and Gamma in me",
"I have Gamma, charlie, and Alpha in me",
),
(
"I have Alpha, charlie, and gamma in me",
"I have gamma, charlie, and alpha in me",
"I have alpha, charlie, and Gamma in me",
"I have Gamma, Charlie, and ALPHA in me",
),
case_sensitive=True,
)
self._test_matching(
'brown fox "lazy dogs"',
"MATCH_ALL",
(
"the quick brown fox jumped over the lazy dogs",
"the quick brown fox jumped over the lazy dogs",
),
(
"the quick Brown fox jumped over the lazy dogs",
"the quick brown Fox jumped over the lazy dogs",
"the quick brown fox jumped over the Lazy dogs",
"the quick brown fox jumped over the lazy Dogs",
),
case_sensitive=True,
)
def test_match_any(self):
self._test_matching(
"alpha charlie gamma",
"MATCH_ANY",
(
"I have alpha in me",
"I have charlie in me",
"I have gamma in me",
"I have alpha, charlie, and gamma in me",
"I have alpha and charlie in me",
),
(
"I have Alpha in me",
"I have chaRLie in me",
"I have gamMA in me",
"I have aLPha, cHArlie, and gAMma in me",
"I have AlphA and CharlIe in me",
),
case_sensitive=True,
)
self._test_matching(
"Alpha Charlie Gamma",
"MATCH_ANY",
(
"I have Alpha in me",
"I have Charlie in me",
"I have Gamma in me",
"I have Alpha, Charlie, and Gamma in me",
"I have Alpha and Charlie in me",
),
(
"I have alpha in me",
"I have ChaRLie in me",
"I have GamMA in me",
"I have ALPha, CHArlie, and GAMma in me",
"I have AlphA and CharlIe in me",
),
case_sensitive=True,
)
self._test_matching(
'"brown fox" " lazy dogs "',
"MATCH_ANY",
(
"the quick brown fox",
"jumped over the lazy dogs.",
),
(
"the quick Brown fox",
"jumped over the lazy Dogs.",
),
case_sensitive=True,
)
def test_match_literal(self):
self._test_matching(
"alpha charlie gamma",
"MATCH_LITERAL",
("I have 'alpha charlie gamma' in me",),
(
"I have 'Alpha charlie gamma' in me",
"I have 'alpha Charlie gamma' in me",
"I have 'alpha charlie Gamma' in me",
"I have 'Alpha Charlie Gamma' in me",
),
case_sensitive=True,
)
self._test_matching(
"Alpha Charlie Gamma",
"MATCH_LITERAL",
("I have 'Alpha Charlie Gamma' in me",),
(
"I have 'Alpha charlie gamma' in me",
"I have 'alpha Charlie gamma' in me",
"I have 'alpha charlie Gamma' in me",
"I have 'alpha charlie gamma' in me",
),
case_sensitive=True,
)
def test_match_regex(self):
self._test_matching(
r"alpha\w+gamma",
"MATCH_REGEX",
(
"I have alpha_and_gamma in me",
"I have alphas_and_gamma in me",
),
(
"I have Alpha_and_Gamma in me",
"I have alpHAs_and_gaMMa in me",
),
case_sensitive=True,
)
self._test_matching(
r"Alpha\w+gamma",
"MATCH_REGEX",
(
"I have Alpha_and_gamma in me",
"I have Alphas_and_gamma in me",
),
(
"I have Alpha_and_Gamma in me",
"I have alphas_and_gamma in me",
),
case_sensitive=True,
)
@override_settings(POST_CONSUME_SCRIPT=None)
class TestDocumentConsumptionFinishedSignal(TestCase):
"""

View File

@@ -1,7 +1,10 @@
import os
import shutil
import tempfile
from unittest import mock
from django.conf import settings
from django.test import override_settings
from django.test import TestCase
from django.utils import timezone
from documents import tasks
@@ -12,6 +15,7 @@ from documents.models import Tag
from documents.sanity_checker import SanityCheckFailedException
from documents.sanity_checker import SanityCheckMessages
from documents.tests.utils import DirectoriesMixin
from PIL import Image
class TestTasks(DirectoriesMixin, TestCase):
@@ -89,6 +93,318 @@ class TestTasks(DirectoriesMixin, TestCase):
mtime3 = os.stat(settings.MODEL_FILE).st_mtime
self.assertNotEqual(mtime2, mtime3)
def test_barcode_reader(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-PATCHT.png",
)
img = Image.open(test_file)
separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
self.assertEqual(tasks.barcode_reader(img), [separator_barcode])
def test_barcode_reader2(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t.pbm",
)
img = Image.open(test_file)
separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
self.assertEqual(tasks.barcode_reader(img), [separator_barcode])
def test_barcode_reader_distorsion(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-PATCHT-distorsion.png",
)
img = Image.open(test_file)
separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
self.assertEqual(tasks.barcode_reader(img), [separator_barcode])
def test_barcode_reader_distorsion2(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-PATCHT-distorsion2.png",
)
img = Image.open(test_file)
separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
self.assertEqual(tasks.barcode_reader(img), [separator_barcode])
def test_barcode_reader_unreadable(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-PATCHT-unreadable.png",
)
img = Image.open(test_file)
self.assertEqual(tasks.barcode_reader(img), [])
def test_barcode_reader_qr(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"qr-code-PATCHT.png",
)
img = Image.open(test_file)
separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
self.assertEqual(tasks.barcode_reader(img), [separator_barcode])
def test_barcode_reader_128(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-128-PATCHT.png",
)
img = Image.open(test_file)
separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
self.assertEqual(tasks.barcode_reader(img), [separator_barcode])
def test_barcode_reader_no_barcode(self):
test_file = os.path.join(os.path.dirname(__file__), "samples", "simple.png")
img = Image.open(test_file)
self.assertEqual(tasks.barcode_reader(img), [])
def test_barcode_reader_custom_separator(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-custom.png",
)
img = Image.open(test_file)
self.assertEqual(tasks.barcode_reader(img), ["CUSTOM BARCODE"])
def test_barcode_reader_custom_qr_separator(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-qr-custom.png",
)
img = Image.open(test_file)
self.assertEqual(tasks.barcode_reader(img), ["CUSTOM BARCODE"])
def test_barcode_reader_custom_128_separator(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-128-custom.png",
)
img = Image.open(test_file)
self.assertEqual(tasks.barcode_reader(img), ["CUSTOM BARCODE"])
def test_scan_file_for_separating_barcodes(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [0])
def test_scan_file_for_separating_barcodes2(self):
test_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf")
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [])
def test_scan_file_for_separating_barcodes3(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t-middle.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [1])
def test_scan_file_for_separating_barcodes4(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"several-patcht-codes.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [2, 5])
def test_scan_file_for_separating_barcodes_upsidedown(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t-middle_reverse.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [1])
def test_scan_file_for_separating_qr_barcodes(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t-qr.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [0])
@override_settings(CONSUMER_BARCODE_STRING="CUSTOM BARCODE")
def test_scan_file_for_separating_custom_barcodes(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-custom.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [0])
@override_settings(CONSUMER_BARCODE_STRING="CUSTOM BARCODE")
def test_scan_file_for_separating_custom_qr_barcodes(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-qr-custom.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [0])
@override_settings(CONSUMER_BARCODE_STRING="CUSTOM BARCODE")
def test_scan_file_for_separating_custom_128_barcodes(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-128-custom.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [0])
def test_scan_file_for_separating_wrong_qr_barcodes(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-custom.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [])
def test_separate_pages(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t-middle.pdf",
)
pages = tasks.separate_pages(test_file, [1])
self.assertEqual(len(pages), 2)
def test_separate_pages_no_list(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t-middle.pdf",
)
with self.assertLogs("paperless.tasks", level="WARNING") as cm:
pages = tasks.separate_pages(test_file, [])
self.assertEqual(pages, [])
self.assertEqual(
cm.output,
[
f"WARNING:paperless.tasks:No pages to split on!",
],
)
def test_save_to_dir(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t.pdf",
)
tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR)
tasks.save_to_dir(test_file, target_dir=tempdir)
target_file = os.path.join(tempdir, "patch-code-t.pdf")
self.assertTrue(os.path.isfile(target_file))
def test_save_to_dir2(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t.pdf",
)
nonexistingdir = "/nowhere"
if os.path.isdir(nonexistingdir):
self.fail("non-existing dir exists")
else:
with self.assertLogs("paperless.tasks", level="WARNING") as cm:
tasks.save_to_dir(test_file, target_dir=nonexistingdir)
self.assertEqual(
cm.output,
[
f"WARNING:paperless.tasks:{str(test_file)} or {str(nonexistingdir)} don't exist.",
],
)
def test_save_to_dir3(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t.pdf",
)
tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR)
tasks.save_to_dir(test_file, newname="newname.pdf", target_dir=tempdir)
target_file = os.path.join(tempdir, "newname.pdf")
self.assertTrue(os.path.isfile(target_file))
def test_barcode_splitter(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t-middle.pdf",
)
tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR)
separators = tasks.scan_file_for_separating_barcodes(test_file)
self.assertTrue(separators)
document_list = tasks.separate_pages(test_file, separators)
self.assertTrue(document_list)
for document in document_list:
tasks.save_to_dir(document, target_dir=tempdir)
target_file1 = os.path.join(tempdir, "patch-code-t-middle_document_0.pdf")
target_file2 = os.path.join(tempdir, "patch-code-t-middle_document_1.pdf")
self.assertTrue(os.path.isfile(target_file1))
self.assertTrue(os.path.isfile(target_file2))
@override_settings(CONSUMER_ENABLE_BARCODES=True)
def test_consume_barcode_file(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t-middle.pdf",
)
dst = os.path.join(settings.SCRATCH_DIR, "patch-code-t-middle.pd")
shutil.copy(test_file, dst)
self.assertEqual(tasks.consume_file(dst), "File successfully split")
@mock.patch("documents.tasks.sanity_checker.check_sanity")
def test_sanity_check_success(self, m):
m.return_value = SanityCheckMessages()

View File

@@ -1,12 +1,14 @@
import json
import logging
import os
import tempfile
import urllib
import uuid
import zipfile
from datetime import datetime
from time import mktime
from unicodedata import normalize
from urllib.parse import quote_plus
from urllib.parse import quote
from django.conf import settings
from django.db.models import Case
@@ -24,6 +26,8 @@ from django.views.decorators.cache import cache_control
from django.views.generic import TemplateView
from django_filters.rest_framework import DjangoFilterBackend
from django_q.tasks import async_task
from packaging import version as packaging_version
from paperless import version
from paperless.db import GnuPG
from paperless.views import StandardPagination
from rest_framework import parsers
@@ -244,7 +248,7 @@ class DocumentViewSet(
# RFC 5987 addresses this issue
# see https://datatracker.ietf.org/doc/html/rfc5987#section-4.2
filename_normalized = normalize("NFKD", filename).encode("ascii", "ignore")
filename_encoded = quote_plus(filename)
filename_encoded = quote(filename)
content_disposition = (
f"{disposition}; "
f'filename="{filename_normalized}"; '
@@ -666,3 +670,40 @@ class BulkDownloadView(GenericAPIView):
)
return response
class RemoteVersionView(GenericAPIView):
def get(self, request, format=None):
remote_version = "0.0.0"
is_greater_than_current = False
# TODO: this can likely be removed when frontend settings are saved to DB
feature_is_set = settings.ENABLE_UPDATE_CHECK != "default"
if feature_is_set and settings.ENABLE_UPDATE_CHECK:
try:
with urllib.request.urlopen(
"https://api.github.com/repos/"
+ "paperless-ngx/paperless-ngx/releases/latest",
) as response:
remote = response.read().decode("utf-8")
try:
remote_json = json.loads(remote)
remote_version = remote_json["tag_name"].replace("ngx-", "")
except ValueError:
logger.debug("An error occured parsing remote version json")
except urllib.error.URLError:
logger.debug("An error occured checking for available updates")
current_version = ".".join([str(_) for _ in version.__version__[:3]])
is_greater_than_current = packaging_version.parse(
remote_version,
) > packaging_version.parse(
current_version,
)
return Response(
{
"version": remote_version,
"update_available": is_greater_than_current,
"feature_is_set": feature_is_set,
},
)

View File

@@ -0,0 +1,714 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-03-02 11:20-0800\n"
"PO-Revision-Date: 2022-03-31 10:58\n"
"Last-Translator: \n"
"Language-Team: Belarusian\n"
"Language: be_BY\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || n%10>=5 && n%10<=9 || n%100>=11 && n%100<=14 ? 2 : 3);\n"
"X-Crowdin-Project: paperless-ngx\n"
"X-Crowdin-Project-ID: 500308\n"
"X-Crowdin-Language: be\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 14\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Дакументы"
#: documents/models.py:32
msgid "Any word"
msgstr "Любое слова"
#: documents/models.py:33
msgid "All words"
msgstr "Усе словы"
#: documents/models.py:34
msgid "Exact match"
msgstr "Дакладнае супадзенне"
#: documents/models.py:35
msgid "Regular expression"
msgstr "Рэгулярны выраз"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Невыразнае слова"
#: documents/models.py:37
msgid "Automatic"
msgstr "Аўтаматычна"
#: documents/models.py:40 documents/models.py:314 paperless_mail/models.py:23
#: paperless_mail/models.py:107
msgid "name"
msgstr "назва"
#: documents/models.py:42
msgid "match"
msgstr "супадзенне"
#: documents/models.py:45
msgid "matching algorithm"
msgstr "алгарытм супастаўлення"
#: documents/models.py:48
msgid "is insensitive"
msgstr "без уліку рэгістра"
#: documents/models.py:61 documents/models.py:104
msgid "correspondent"
msgstr "карэспандэнт"
#: documents/models.py:62
msgid "correspondents"
msgstr "карэспандэнты"
#: documents/models.py:67
msgid "color"
msgstr "колер"
#: documents/models.py:70
msgid "is inbox tag"
msgstr "гэта ўваходны тэг"
#: documents/models.py:73
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Пазначыць гэты тэг як тэг папкі \"Уваходныя\": Усе нядаўна спажытыя дакументы будуць пазначаны тэгамі \"Уваходныя\"."
#: documents/models.py:79
msgid "tag"
msgstr "тэг"
#: documents/models.py:80 documents/models.py:130
msgid "tags"
msgstr "тэгі"
#: documents/models.py:85 documents/models.py:115
msgid "document type"
msgstr "тып дакумента"
#: documents/models.py:86
msgid "document types"
msgstr "тыпы дакументаў"
#: documents/models.py:94
msgid "Unencrypted"
msgstr "Незашыфраваны"
#: documents/models.py:95
msgid "Encrypted with GNU Privacy Guard"
msgstr "Зашыфравана з дапамогай GNU Privacy Guard"
#: documents/models.py:107
msgid "title"
msgstr "назва"
#: documents/models.py:119
msgid "content"
msgstr "змест"
#: documents/models.py:122
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "Неапрацаваныя тэкставыя даныя дакумента. Гэта поле ў асноўным выкарыстоўваецца для пошуку."
#: documents/models.py:127
msgid "mime type"
msgstr "тып MIME"
#: documents/models.py:134
msgid "checksum"
msgstr "кантрольная сума"
#: documents/models.py:138
msgid "The checksum of the original document."
msgstr "Кантрольная сума зыходнага дакумента."
#: documents/models.py:142
msgid "archive checksum"
msgstr "кантрольная сума архіва"
#: documents/models.py:147
msgid "The checksum of the archived document."
msgstr "Кантрольная сума архіўнага дакумента."
#: documents/models.py:150 documents/models.py:295
msgid "created"
msgstr "створаны"
#: documents/models.py:153
msgid "modified"
msgstr "мадыфікаваны"
#: documents/models.py:157
msgid "storage type"
msgstr "тып захоўвання"
#: documents/models.py:165
msgid "added"
msgstr "дададзена"
#: documents/models.py:169
msgid "filename"
msgstr "імя файла"
#: documents/models.py:175
msgid "Current filename in storage"
msgstr "Цяперашняе імя файла ў сховішчы"
#: documents/models.py:179
msgid "archive filename"
msgstr "імя файла архіва"
#: documents/models.py:185
msgid "Current archive filename in storage"
msgstr "Цяперашняе імя файла архіва ў сховішчы"
#: documents/models.py:189
msgid "archive serial number"
msgstr "парадкавы нумар архіва"
#: documents/models.py:195
msgid "The position of this document in your physical document archive."
msgstr "Пазіцыя гэтага дакумента ў вашым фізічным архіве дакументаў."
#: documents/models.py:201
msgid "document"
msgstr "дакумент"
#: documents/models.py:202
msgid "documents"
msgstr "дакументы"
#: documents/models.py:280
msgid "debug"
msgstr "адладка"
#: documents/models.py:281
msgid "information"
msgstr "інфармацыя"
#: documents/models.py:282
msgid "warning"
msgstr "папярэджанне"
#: documents/models.py:283
msgid "error"
msgstr "памылка"
#: documents/models.py:284
msgid "critical"
msgstr "крытычны"
#: documents/models.py:287
msgid "group"
msgstr "група"
#: documents/models.py:289
msgid "message"
msgstr "паведамленне"
#: documents/models.py:292
msgid "level"
msgstr "узровень"
#: documents/models.py:299
msgid "log"
msgstr "лог"
#: documents/models.py:300
msgid "logs"
msgstr "логі"
#: documents/models.py:310 documents/models.py:360
msgid "saved view"
msgstr "захаваны выгляд"
#: documents/models.py:311
msgid "saved views"
msgstr "захаваныя выгляды"
#: documents/models.py:313
msgid "user"
msgstr "карыстальнік"
#: documents/models.py:317
msgid "show on dashboard"
msgstr "паказаць на панэлі"
#: documents/models.py:320
msgid "show in sidebar"
msgstr "паказаць у бакавой панэлі"
#: documents/models.py:324
msgid "sort field"
msgstr "поле сартавання"
#: documents/models.py:326
msgid "sort reverse"
msgstr "сартаваць у адваротным парадку"
#: documents/models.py:331
msgid "title contains"
msgstr "назва змяшчае"
#: documents/models.py:332
msgid "content contains"
msgstr "змест змяшчае"
#: documents/models.py:333
msgid "ASN is"
msgstr "ASN"
#: documents/models.py:334
msgid "correspondent is"
msgstr "карэспандэнт"
#: documents/models.py:335
msgid "document type is"
msgstr "тып дакумента"
#: documents/models.py:336
msgid "is in inbox"
msgstr "ва ўваходных"
#: documents/models.py:337
msgid "has tag"
msgstr "мае тэг"
#: documents/models.py:338
msgid "has any tag"
msgstr "мае любы тэг"
#: documents/models.py:339
msgid "created before"
msgstr "створана перад"
#: documents/models.py:340
msgid "created after"
msgstr "створана пасля"
#: documents/models.py:341
msgid "created year is"
msgstr "год стварэння"
#: documents/models.py:342
msgid "created month is"
msgstr "месяц стварэння"
#: documents/models.py:343
msgid "created day is"
msgstr "дзень стварэння"
#: documents/models.py:344
msgid "added before"
msgstr "даданы перад"
#: documents/models.py:345
msgid "added after"
msgstr "даданы пасля"
#: documents/models.py:346
msgid "modified before"
msgstr "зменены перад"
#: documents/models.py:347
msgid "modified after"
msgstr "зменены пасля"
#: documents/models.py:348
msgid "does not have tag"
msgstr "не мае тэга"
#: documents/models.py:349
msgid "does not have ASN"
msgstr "не мае ASN"
#: documents/models.py:350
msgid "title or content contains"
msgstr "назва або змест смяшчае"
#: documents/models.py:351
msgid "fulltext query"
msgstr "поўнатэкставы запыт"
#: documents/models.py:352
msgid "more like this"
msgstr "больш падобнага"
#: documents/models.py:353
msgid "has tags in"
msgstr "мае тэгі ў"
#: documents/models.py:363
msgid "rule type"
msgstr "тып правіла"
#: documents/models.py:365
msgid "value"
msgstr "значэнне"
#: documents/models.py:368
msgid "filter rule"
msgstr "правіла фільтрацыі"
#: documents/models.py:369
msgid "filter rules"
msgstr "правілы фільтрацыі"
#: documents/serialisers.py:64
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "Няправільны рэгулярны выраз: %(error)s"
#: documents/serialisers.py:185
msgid "Invalid color."
msgstr "Няправільны колер."
#: documents/serialisers.py:459
#, python-format
msgid "File type %(type)s not supported"
msgstr "Тып файла %(type)s не падтрымліваецца"
#: documents/templates/index.html:22
msgid "Paperless-ngx is loading..."
msgstr "Paperless-ngx загружаецца..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ngx signed out"
msgstr "Выкананы выхад з Paperless-ngx"
#: documents/templates/registration/logged_out.html:59
msgid "You have been successfully logged out. Bye!"
msgstr "Вы паспяхова выйшлі з сістэмы. Да пабачэння!"
#: documents/templates/registration/logged_out.html:60
msgid "Sign in again"
msgstr "Увайсці зноў"
#: documents/templates/registration/login.html:15
msgid "Paperless-ngx sign in"
msgstr "Увайсці ў Paperless-ngx"
#: documents/templates/registration/login.html:61
msgid "Please sign in."
msgstr "Калі ласка, увайдзіце."
#: documents/templates/registration/login.html:64
msgid "Your username and password didn't match. Please try again."
msgstr "Няправільныя імя карыстальніка або пароль! Паспрабуйце яшчэ раз."
#: documents/templates/registration/login.html:67
msgid "Username"
msgstr "Імя карыстальніка"
#: documents/templates/registration/login.html:68
msgid "Password"
msgstr "Пароль"
#: documents/templates/registration/login.html:73
msgid "Sign in"
msgstr "Увайсці"
#: paperless/settings.py:299
msgid "English (US)"
msgstr "Англійская (ЗША)"
#: paperless/settings.py:300
msgid "Czech"
msgstr "Чэшская"
#: paperless/settings.py:301
msgid "Danish"
msgstr "Дацкая"
#: paperless/settings.py:302
msgid "German"
msgstr "Нямецкая"
#: paperless/settings.py:303
msgid "English (GB)"
msgstr "Англійская (Вялікабрытанія)"
#: paperless/settings.py:304
msgid "Spanish"
msgstr "Іспанская"
#: paperless/settings.py:305
msgid "French"
msgstr "Французская"
#: paperless/settings.py:306
msgid "Italian"
msgstr "Італьянская"
#: paperless/settings.py:307
msgid "Luxembourgish"
msgstr "Люксембургская"
#: paperless/settings.py:308
msgid "Dutch"
msgstr "Нідэрландская"
#: paperless/settings.py:309
msgid "Polish"
msgstr "Польская"
#: paperless/settings.py:310
msgid "Portuguese (Brazil)"
msgstr "Партугальская (Бразілія)"
#: paperless/settings.py:311
msgid "Portuguese"
msgstr "Партугальская"
#: paperless/settings.py:312
msgid "Romanian"
msgstr "Румынская"
#: paperless/settings.py:313
msgid "Russian"
msgstr "Руская"
#: paperless/settings.py:314
msgid "Swedish"
msgstr "Шведская"
#: paperless/urls.py:139
msgid "Paperless-ngx administration"
msgstr "Адміністраванне Paperless-ngx"
#: paperless_mail/admin.py:29
msgid "Authentication"
msgstr "Аўтэнтыфікацыя"
#: paperless_mail/admin.py:30
msgid "Advanced settings"
msgstr "Пашыраныя налады"
#: paperless_mail/admin.py:47
msgid "Filter"
msgstr "Фільтр"
#: paperless_mail/admin.py:50
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless-ngx будзе апрацоўваць толькі лісты, якія адпавядаюць УСІМ фільтрам, прыведзеным ніжэй."
#: paperless_mail/admin.py:64
msgid "Actions"
msgstr "Дзеянні"
#: paperless_mail/admin.py:67
msgid "The action applied to the mail. This action is only performed when documents were consumed from the mail. Mails without attachments will remain entirely untouched."
msgstr "Дзеянне распаўсюджваецца на пошту. Гэта дзеянне выконваецца толькі тады, калі дакументы былі спажыты з пошты. Пошты без укладанняў застануцца цалкам некранутымі."
#: paperless_mail/admin.py:75
msgid "Metadata"
msgstr "Метаданыя"
#: paperless_mail/admin.py:78
msgid "Assign metadata to documents consumed from this rule automatically. If you do not assign tags, types or correspondents here, paperless will still process all matching rules that you have defined."
msgstr "Аўтаматычна прызначаць метададзеныя дакументам, атрыманым з гэтага правіла. Калі вы не прызначаеце тут тэгі, тыпы ці карэспандэнты, Paperless-ngx усё роўна будуць апрацоўваць усе адпаведныя правілы, якія вы вызначылі."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Paperless-ngx пошта"
#: paperless_mail/models.py:10
msgid "mail account"
msgstr "паштовы акаўнт"
#: paperless_mail/models.py:11
msgid "mail accounts"
msgstr "паштовыя акаўнты"
#: paperless_mail/models.py:18
msgid "No encryption"
msgstr "Без шыфравання"
#: paperless_mail/models.py:19
msgid "Use SSL"
msgstr "Выкарыстоўваць SSL"
#: paperless_mail/models.py:20
msgid "Use STARTTLS"
msgstr "Выкарыстоўваць STARTTLS"
#: paperless_mail/models.py:25
msgid "IMAP server"
msgstr "Сервер IMAP"
#: paperless_mail/models.py:28
msgid "IMAP port"
msgstr "Порт IMAP"
#: paperless_mail/models.py:32
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "Звычайна гэта 143 для незашыфраваных і STARTTLS злучэнняў і 993 для злучэнняў SSL."
#: paperless_mail/models.py:38
msgid "IMAP security"
msgstr "Бяспека IMAP"
#: paperless_mail/models.py:41
msgid "username"
msgstr "імя карыстальніка"
#: paperless_mail/models.py:43
msgid "password"
msgstr "пароль"
#: paperless_mail/models.py:46
msgid "character set"
msgstr "кадзіроўка"
#: paperless_mail/models.py:50
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr "Кадзіроўка для сувязі з паштовым серверам, напрыклад «UTF-8» або «US-ASCII»."
#: paperless_mail/models.py:61
msgid "mail rule"
msgstr "правіла пошты"
#: paperless_mail/models.py:62
msgid "mail rules"
msgstr "правілы пошты"
#: paperless_mail/models.py:68
msgid "Only process attachments."
msgstr "Апрацоўваць толькі ўкладанні."
#: paperless_mail/models.py:71
msgid "Process all files, including 'inline' attachments."
msgstr "Апрацоўваць усе файлы, уключаючы 'убудаваныя' укладанні."
#: paperless_mail/models.py:81
msgid "Mark as read, don't process read mails"
msgstr "Пазначыць як прачытанае, не апрацоўваць прачытаныя лісты"
#: paperless_mail/models.py:82
msgid "Flag the mail, don't process flagged mails"
msgstr "Пазначыць пошту, не апрацоўваць пазначаныя лісты"
#: paperless_mail/models.py:83
msgid "Move to specified folder"
msgstr "Перамясціць у паказаную папку"
#: paperless_mail/models.py:84
msgid "Delete"
msgstr "Выдаліць"
#: paperless_mail/models.py:91
msgid "Use subject as title"
msgstr "Тэма ў якасці загалоўка"
#: paperless_mail/models.py:92
msgid "Use attachment filename as title"
msgstr "Выкарыстоўваць імя ўкладзенага файла як загаловак"
#: paperless_mail/models.py:101
msgid "Do not assign a correspondent"
msgstr "Не прызначаць карэспандэнта"
#: paperless_mail/models.py:102
msgid "Use mail address"
msgstr "Выкарыстоўваць email адрас"
#: paperless_mail/models.py:103
msgid "Use name (or mail address if not available)"
msgstr "Выкарыстоўваць імя (або адрас электроннай пошты, калі недаступна)"
#: paperless_mail/models.py:104
msgid "Use correspondent selected below"
msgstr "Выкарыстоўваць карэспандэнта, абранага ніжэй"
#: paperless_mail/models.py:109
msgid "order"
msgstr "парадак"
#: paperless_mail/models.py:115
msgid "account"
msgstr "ўліковы запіс"
#: paperless_mail/models.py:119
msgid "folder"
msgstr "каталог"
#: paperless_mail/models.py:122
msgid "Subfolders must be separated by dots."
msgstr "Падкаталогі павінны быць падзелены кропкамі."
#: paperless_mail/models.py:126
msgid "filter from"
msgstr "фільтр па адпраўніку"
#: paperless_mail/models.py:129
msgid "filter subject"
msgstr "фільтр па тэме"
#: paperless_mail/models.py:132
msgid "filter body"
msgstr "фільтр па тэксце паведамлення"
#: paperless_mail/models.py:136
msgid "filter attachment filename"
msgstr "фільтр па імені ўкладання"
#: paperless_mail/models.py:141
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "Апрацоўваць толькі дакументы, якія цалкам супадаюць з імем файла (калі яно пазначана). Маскі, напрыклад *.pdf ці *рахунак*, дазволеныя. Без уліку рэгістра."
#: paperless_mail/models.py:148
msgid "maximum age"
msgstr "максімальны ўзрост"
#: paperless_mail/models.py:148
msgid "Specified in days."
msgstr "Указваецца ў днях."
#: paperless_mail/models.py:152
msgid "attachment type"
msgstr "тып укладання"
#: paperless_mail/models.py:156
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "Убудаваныя ўкладанні ўключаюць убудаваныя выявы, таму лепш камбінаваць гэты варыянт з фільтрам імёнаў файла."
#: paperless_mail/models.py:162
msgid "action"
msgstr "дзеянне"
#: paperless_mail/models.py:168
msgid "action parameter"
msgstr "параметр дзеяння"
#: paperless_mail/models.py:173
msgid "Additional parameter for the action selected above, i.e., the target folder of the move to folder action. Subfolders must be separated by dots."
msgstr "Дадатковы параметр для дзеяння, абранага вышэй, гэта значыць, мэтавая папка дзеяння перамяшчэння ў папку. Падпапкі павінны быць падзеленыя кропкамі."
#: paperless_mail/models.py:181
msgid "assign title from"
msgstr "прызначыць загаловак з"
#: paperless_mail/models.py:189
msgid "assign this tag"
msgstr "прызначыць гэты тэг"
#: paperless_mail/models.py:197
msgid "assign this document type"
msgstr "прызначыць гэты тып дакумента"
#: paperless_mail/models.py:201
msgid "assign correspondent from"
msgstr "прызначыць карэспандэнта з"
#: paperless_mail/models.py:211
msgid "assign this correspondent"
msgstr "прызначыць гэтага карэспандэнта"

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-03-02 11:20-0800\n"
"PO-Revision-Date: 2022-03-02 22:29\n"
"PO-Revision-Date: 2022-03-26 21:49\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -72,7 +72,7 @@ msgstr "interlocutores"
#: documents/models.py:67
msgid "color"
msgstr ""
msgstr "color"
#: documents/models.py:70
msgid "is inbox tag"
@@ -200,7 +200,7 @@ msgstr "alerta"
#: documents/models.py:283
msgid "error"
msgstr ""
msgstr "error"
#: documents/models.py:284
msgid "critical"
@@ -220,11 +220,11 @@ msgstr "nivel"
#: documents/models.py:299
msgid "log"
msgstr ""
msgstr "log"
#: documents/models.py:300
msgid "logs"
msgstr ""
msgstr "logs"
#: documents/models.py:310 documents/models.py:360
msgid "saved view"
@@ -344,7 +344,7 @@ msgstr "más contenido similar"
#: documents/models.py:353
msgid "has tags in"
msgstr ""
msgstr "tiene etiquetas en"
#: documents/models.py:363
msgid "rule type"
@@ -378,11 +378,11 @@ msgstr "Tipo de fichero %(type)s no suportado"
#: documents/templates/index.html:22
msgid "Paperless-ngx is loading..."
msgstr ""
msgstr "Paperless-ngx está cargando..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ngx signed out"
msgstr ""
msgstr "Paperless-ngx cerró sesión"
#: documents/templates/registration/logged_out.html:59
msgid "You have been successfully logged out. Bye!"
@@ -394,7 +394,7 @@ msgstr "Iniciar sesión de nuevo"
#: documents/templates/registration/login.html:15
msgid "Paperless-ngx sign in"
msgstr ""
msgstr "Paperless-ngx inicio de sesión"
#: documents/templates/registration/login.html:61
msgid "Please sign in."
@@ -422,11 +422,11 @@ msgstr "Inglés (US)"
#: paperless/settings.py:300
msgid "Czech"
msgstr ""
msgstr "Checo"
#: paperless/settings.py:301
msgid "Danish"
msgstr ""
msgstr "Danés"
#: paperless/settings.py:302
msgid "German"
@@ -450,7 +450,7 @@ msgstr "Italiano"
#: paperless/settings.py:307
msgid "Luxembourgish"
msgstr ""
msgstr "Luxemburgués"
#: paperless/settings.py:308
msgid "Dutch"
@@ -482,7 +482,7 @@ msgstr "Sueco"
#: paperless/urls.py:139
msgid "Paperless-ngx administration"
msgstr ""
msgstr "Administración de Paperless-ngx"
#: paperless_mail/admin.py:29
msgid "Authentication"

View File

@@ -0,0 +1,714 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-03-02 11:20-0800\n"
"PO-Revision-Date: 2022-03-31 08:58\n"
"Last-Translator: \n"
"Language-Team: Finnish\n"
"Language: fi_FI\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: paperless-ngx\n"
"X-Crowdin-Project-ID: 500308\n"
"X-Crowdin-Language: fi\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 14\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Dokumentit"
#: documents/models.py:32
msgid "Any word"
msgstr "Mikä tahansa sana"
#: documents/models.py:33
msgid "All words"
msgstr "Kaikki sanat"
#: documents/models.py:34
msgid "Exact match"
msgstr "Tarkka osuma"
#: documents/models.py:35
msgid "Regular expression"
msgstr "Säännöllinen lauseke (regex)"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Sumea sana"
#: documents/models.py:37
msgid "Automatic"
msgstr "Automaattinen"
#: documents/models.py:40 documents/models.py:314 paperless_mail/models.py:23
#: paperless_mail/models.py:107
msgid "name"
msgstr "nimi"
#: documents/models.py:42
msgid "match"
msgstr "osuma"
#: documents/models.py:45
msgid "matching algorithm"
msgstr "tunnistusalgoritmi"
#: documents/models.py:48
msgid "is insensitive"
msgstr "ei ole herkkä"
#: documents/models.py:61 documents/models.py:104
msgid "correspondent"
msgstr "yhteyshenkilö"
#: documents/models.py:62
msgid "correspondents"
msgstr "yhteyshenkilöt"
#: documents/models.py:67
msgid "color"
msgstr "väri"
#: documents/models.py:70
msgid "is inbox tag"
msgstr "on uusien tunniste"
#: documents/models.py:73
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Merkitsee tämän tunnisteen uusien tunnisteeksi: Kaikille vastasyötetyille tiedostoille annetaan tämä tunniste."
#: documents/models.py:79
msgid "tag"
msgstr "tunniste"
#: documents/models.py:80 documents/models.py:130
msgid "tags"
msgstr "tunnisteet"
#: documents/models.py:85 documents/models.py:115
msgid "document type"
msgstr "asiakirjatyyppi"
#: documents/models.py:86
msgid "document types"
msgstr "asiakirjatyypit"
#: documents/models.py:94
msgid "Unencrypted"
msgstr "Salaamaton"
#: documents/models.py:95
msgid "Encrypted with GNU Privacy Guard"
msgstr "GNU Privacy Guard -salattu"
#: documents/models.py:107
msgid "title"
msgstr "otsikko"
#: documents/models.py:119
msgid "content"
msgstr "sisältö"
#: documents/models.py:122
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "Raaka vain teksti -muotoinen dokumentin sisältö. Kenttää käytetään pääasiassa hakutoiminnossa."
#: documents/models.py:127
msgid "mime type"
msgstr "mime-tyyppi"
#: documents/models.py:134
msgid "checksum"
msgstr "tarkistussumma"
#: documents/models.py:138
msgid "The checksum of the original document."
msgstr "Alkuperäisen dokumentin tarkistussumma."
#: documents/models.py:142
msgid "archive checksum"
msgstr "arkistotarkastussumma"
#: documents/models.py:147
msgid "The checksum of the archived document."
msgstr "Arkistoidun dokumentin tarkistussumma."
#: documents/models.py:150 documents/models.py:295
msgid "created"
msgstr "luotu"
#: documents/models.py:153
msgid "modified"
msgstr "muokattu"
#: documents/models.py:157
msgid "storage type"
msgstr "tallennustilan tyyppi"
#: documents/models.py:165
msgid "added"
msgstr "lisätty"
#: documents/models.py:169
msgid "filename"
msgstr "tiedostonimi"
#: documents/models.py:175
msgid "Current filename in storage"
msgstr "Tiedostonimi tallennustilassa"
#: documents/models.py:179
msgid "archive filename"
msgstr "arkistointitiedostonimi"
#: documents/models.py:185
msgid "Current archive filename in storage"
msgstr "Tämänhetkinen arkistointitiedostoimi tallennustilassa"
#: documents/models.py:189
msgid "archive serial number"
msgstr "arkistointisarjanumero"
#: documents/models.py:195
msgid "The position of this document in your physical document archive."
msgstr "Dokumentin sijainti fyysisessa dokumenttiarkistossa."
#: documents/models.py:201
msgid "document"
msgstr "dokumentti"
#: documents/models.py:202
msgid "documents"
msgstr "dokumentit"
#: documents/models.py:280
msgid "debug"
msgstr "virheenjäljitys"
#: documents/models.py:281
msgid "information"
msgstr "informaatio"
#: documents/models.py:282
msgid "warning"
msgstr "varoitus"
#: documents/models.py:283
msgid "error"
msgstr "virhe"
#: documents/models.py:284
msgid "critical"
msgstr "kriittinen"
#: documents/models.py:287
msgid "group"
msgstr "ryhmä"
#: documents/models.py:289
msgid "message"
msgstr "viesti"
#: documents/models.py:292
msgid "level"
msgstr "taso"
#: documents/models.py:299
msgid "log"
msgstr "loki"
#: documents/models.py:300
msgid "logs"
msgstr "lokit"
#: documents/models.py:310 documents/models.py:360
msgid "saved view"
msgstr "tallennettu näkymä"
#: documents/models.py:311
msgid "saved views"
msgstr "tallennetut näkymät"
#: documents/models.py:313
msgid "user"
msgstr "käyttäjä"
#: documents/models.py:317
msgid "show on dashboard"
msgstr "näytä etusivulla"
#: documents/models.py:320
msgid "show in sidebar"
msgstr "näytä sivupaneelissa"
#: documents/models.py:324
msgid "sort field"
msgstr "lajittelukenttä"
#: documents/models.py:326
msgid "sort reverse"
msgstr "lajittele käänteisesti"
#: documents/models.py:331
msgid "title contains"
msgstr "otsikko sisältää"
#: documents/models.py:332
msgid "content contains"
msgstr "sisältö sisältää"
#: documents/models.py:333
msgid "ASN is"
msgstr "ASN on"
#: documents/models.py:334
msgid "correspondent is"
msgstr "yhteyshenkilö on"
#: documents/models.py:335
msgid "document type is"
msgstr "dokumenttityyppi on"
#: documents/models.py:336
msgid "is in inbox"
msgstr "on uusi"
#: documents/models.py:337
msgid "has tag"
msgstr "on tagattu"
#: documents/models.py:338
msgid "has any tag"
msgstr "on mikä tahansa tagi"
#: documents/models.py:339
msgid "created before"
msgstr "luotu ennen"
#: documents/models.py:340
msgid "created after"
msgstr "luotu jälkeen"
#: documents/models.py:341
msgid "created year is"
msgstr "luotu vuonna"
#: documents/models.py:342
msgid "created month is"
msgstr "luotu kuukautena"
#: documents/models.py:343
msgid "created day is"
msgstr ""
#: documents/models.py:344
msgid "added before"
msgstr ""
#: documents/models.py:345
msgid "added after"
msgstr ""
#: documents/models.py:346
msgid "modified before"
msgstr ""
#: documents/models.py:347
msgid "modified after"
msgstr ""
#: documents/models.py:348
msgid "does not have tag"
msgstr ""
#: documents/models.py:349
msgid "does not have ASN"
msgstr ""
#: documents/models.py:350
msgid "title or content contains"
msgstr ""
#: documents/models.py:351
msgid "fulltext query"
msgstr ""
#: documents/models.py:352
msgid "more like this"
msgstr ""
#: documents/models.py:353
msgid "has tags in"
msgstr ""
#: documents/models.py:363
msgid "rule type"
msgstr ""
#: documents/models.py:365
msgid "value"
msgstr ""
#: documents/models.py:368
msgid "filter rule"
msgstr ""
#: documents/models.py:369
msgid "filter rules"
msgstr ""
#: documents/serialisers.py:64
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr ""
#: documents/serialisers.py:185
msgid "Invalid color."
msgstr ""
#: documents/serialisers.py:459
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
#: documents/templates/index.html:22
msgid "Paperless-ngx is loading..."
msgstr ""
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ngx signed out"
msgstr ""
#: documents/templates/registration/logged_out.html:59
msgid "You have been successfully logged out. Bye!"
msgstr ""
#: documents/templates/registration/logged_out.html:60
msgid "Sign in again"
msgstr ""
#: documents/templates/registration/login.html:15
msgid "Paperless-ngx sign in"
msgstr ""
#: documents/templates/registration/login.html:61
msgid "Please sign in."
msgstr ""
#: documents/templates/registration/login.html:64
msgid "Your username and password didn't match. Please try again."
msgstr ""
#: documents/templates/registration/login.html:67
msgid "Username"
msgstr ""
#: documents/templates/registration/login.html:68
msgid "Password"
msgstr ""
#: documents/templates/registration/login.html:73
msgid "Sign in"
msgstr ""
#: paperless/settings.py:299
msgid "English (US)"
msgstr ""
#: paperless/settings.py:300
msgid "Czech"
msgstr ""
#: paperless/settings.py:301
msgid "Danish"
msgstr ""
#: paperless/settings.py:302
msgid "German"
msgstr ""
#: paperless/settings.py:303
msgid "English (GB)"
msgstr ""
#: paperless/settings.py:304
msgid "Spanish"
msgstr ""
#: paperless/settings.py:305
msgid "French"
msgstr ""
#: paperless/settings.py:306
msgid "Italian"
msgstr ""
#: paperless/settings.py:307
msgid "Luxembourgish"
msgstr ""
#: paperless/settings.py:308
msgid "Dutch"
msgstr ""
#: paperless/settings.py:309
msgid "Polish"
msgstr "puola"
#: paperless/settings.py:310
msgid "Portuguese (Brazil)"
msgstr "portugali (Brasilia)"
#: paperless/settings.py:311
msgid "Portuguese"
msgstr "portugali"
#: paperless/settings.py:312
msgid "Romanian"
msgstr "romania"
#: paperless/settings.py:313
msgid "Russian"
msgstr "venäjä"
#: paperless/settings.py:314
msgid "Swedish"
msgstr "ruotsi"
#: paperless/urls.py:139
msgid "Paperless-ngx administration"
msgstr "Paperless-ngx hallintapaneeli"
#: paperless_mail/admin.py:29
msgid "Authentication"
msgstr "Autentikaatio"
#: paperless_mail/admin.py:30
msgid "Advanced settings"
msgstr "Edistyneet asetukset"
#: paperless_mail/admin.py:47
msgid "Filter"
msgstr "Suodatin"
#: paperless_mail/admin.py:50
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless prosessoi vain sähköpostit jotka sopivat KAIKKIIN annettuihin filttereihin."
#: paperless_mail/admin.py:64
msgid "Actions"
msgstr "Toiminnot"
#: paperless_mail/admin.py:67
msgid "The action applied to the mail. This action is only performed when documents were consumed from the mail. Mails without attachments will remain entirely untouched."
msgstr ""
#: paperless_mail/admin.py:75
msgid "Metadata"
msgstr "Metatiedot"
#: paperless_mail/admin.py:78
msgid "Assign metadata to documents consumed from this rule automatically. If you do not assign tags, types or correspondents here, paperless will still process all matching rules that you have defined."
msgstr "Määritä tuodun dokumentin metadata tämän säännön perusteella automaattisesti. Jos et aseta tageja, tyyppejä tai omistajia täällä, Paperless prosessoi silti kaikki sopivat määritellyt säännöt."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Paperless-sähköposti"
#: paperless_mail/models.py:10
msgid "mail account"
msgstr "sähköpostitili"
#: paperless_mail/models.py:11
msgid "mail accounts"
msgstr "sähköpostitilit"
#: paperless_mail/models.py:18
msgid "No encryption"
msgstr "Ei salausta"
#: paperless_mail/models.py:19
msgid "Use SSL"
msgstr "Käytä SSL-salausta"
#: paperless_mail/models.py:20
msgid "Use STARTTLS"
msgstr "Käytä STARTTLS-salausta"
#: paperless_mail/models.py:25
msgid "IMAP server"
msgstr "IMAP-palvelin"
#: paperless_mail/models.py:28
msgid "IMAP port"
msgstr "IMAP-portti"
#: paperless_mail/models.py:32
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "Tämä on yleensä 142 salaamattomille sekä STARTTLS-yhteyksille, ja 993 SSL-yhteyksille."
#: paperless_mail/models.py:38
msgid "IMAP security"
msgstr "IMAP-suojaus"
#: paperless_mail/models.py:41
msgid "username"
msgstr "käyttäjänimi"
#: paperless_mail/models.py:43
msgid "password"
msgstr "salasana"
#: paperless_mail/models.py:46
msgid "character set"
msgstr "merkistö"
#: paperless_mail/models.py:50
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr "Merkistö määritellään sähköpostipalvelimen kanssa komminukointia varten. Se voi olla esimerkiksi \"UTF-8\" tai \"US-ASCII\"."
#: paperless_mail/models.py:61
msgid "mail rule"
msgstr "sähköpostisääntö"
#: paperless_mail/models.py:62
msgid "mail rules"
msgstr "sähköpostisäännöt"
#: paperless_mail/models.py:68
msgid "Only process attachments."
msgstr "Prosessoi vain liitteet."
#: paperless_mail/models.py:71
msgid "Process all files, including 'inline' attachments."
msgstr "Prosessoi kaikki tiedostot, sisältäen \"inline\"-liitteet."
#: paperless_mail/models.py:81
msgid "Mark as read, don't process read mails"
msgstr "Merkitse luetuksi, älä prosessoi luettuja sähköposteja"
#: paperless_mail/models.py:82
msgid "Flag the mail, don't process flagged mails"
msgstr ""
#: paperless_mail/models.py:83
msgid "Move to specified folder"
msgstr ""
#: paperless_mail/models.py:84
msgid "Delete"
msgstr ""
#: paperless_mail/models.py:91
msgid "Use subject as title"
msgstr ""
#: paperless_mail/models.py:92
msgid "Use attachment filename as title"
msgstr ""
#: paperless_mail/models.py:101
msgid "Do not assign a correspondent"
msgstr ""
#: paperless_mail/models.py:102
msgid "Use mail address"
msgstr ""
#: paperless_mail/models.py:103
msgid "Use name (or mail address if not available)"
msgstr ""
#: paperless_mail/models.py:104
msgid "Use correspondent selected below"
msgstr ""
#: paperless_mail/models.py:109
msgid "order"
msgstr ""
#: paperless_mail/models.py:115
msgid "account"
msgstr ""
#: paperless_mail/models.py:119
msgid "folder"
msgstr ""
#: paperless_mail/models.py:122
msgid "Subfolders must be separated by dots."
msgstr ""
#: paperless_mail/models.py:126
msgid "filter from"
msgstr ""
#: paperless_mail/models.py:129
msgid "filter subject"
msgstr ""
#: paperless_mail/models.py:132
msgid "filter body"
msgstr ""
#: paperless_mail/models.py:136
msgid "filter attachment filename"
msgstr ""
#: paperless_mail/models.py:141
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr ""
#: paperless_mail/models.py:148
msgid "maximum age"
msgstr ""
#: paperless_mail/models.py:148
msgid "Specified in days."
msgstr ""
#: paperless_mail/models.py:152
msgid "attachment type"
msgstr ""
#: paperless_mail/models.py:156
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr ""
#: paperless_mail/models.py:162
msgid "action"
msgstr ""
#: paperless_mail/models.py:168
msgid "action parameter"
msgstr ""
#: paperless_mail/models.py:173
msgid "Additional parameter for the action selected above, i.e., the target folder of the move to folder action. Subfolders must be separated by dots."
msgstr ""
#: paperless_mail/models.py:181
msgid "assign title from"
msgstr ""
#: paperless_mail/models.py:189
msgid "assign this tag"
msgstr ""
#: paperless_mail/models.py:197
msgid "assign this document type"
msgstr ""
#: paperless_mail/models.py:201
msgid "assign correspondent from"
msgstr ""
#: paperless_mail/models.py:211
msgid "assign this correspondent"
msgstr ""

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-03-02 11:20-0800\n"
"PO-Revision-Date: 2022-03-24 18:56\n"
"PO-Revision-Date: 2022-03-30 11:46\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -344,7 +344,7 @@ msgstr "больше похожих"
#: documents/models.py:353
msgid "has tags in"
msgstr ""
msgstr "имеет теги в"
#: documents/models.py:363
msgid "rule type"
@@ -382,7 +382,7 @@ msgstr "Paperless-ngx загружается..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ngx signed out"
msgstr ""
msgstr "Выполнен выход из Paperless-ngx"
#: documents/templates/registration/logged_out.html:59
msgid "You have been successfully logged out. Bye!"
@@ -394,7 +394,7 @@ msgstr "Войти снова"
#: documents/templates/registration/login.html:15
msgid "Paperless-ngx sign in"
msgstr ""
msgstr "Войти в Paperless-ngx"
#: documents/templates/registration/login.html:61
msgid "Please sign in."
@@ -450,7 +450,7 @@ msgstr "Итальянский"
#: paperless/settings.py:307
msgid "Luxembourgish"
msgstr ""
msgstr "Люксембургский"
#: paperless/settings.py:308
msgid "Dutch"
@@ -462,7 +462,7 @@ msgstr "Польский"
#: paperless/settings.py:310
msgid "Portuguese (Brazil)"
msgstr ""
msgstr "Португальский (Бразилия)"
#: paperless/settings.py:311
msgid "Portuguese"
@@ -482,7 +482,7 @@ msgstr "Шведский"
#: paperless/urls.py:139
msgid "Paperless-ngx administration"
msgstr ""
msgstr "Администрирование Paperless-ngx"
#: paperless_mail/admin.py:29
msgid "Authentication"

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-03-02 11:20-0800\n"
"PO-Revision-Date: 2022-03-22 08:43\n"
"PO-Revision-Date: 2022-03-27 17:08\n"
"Last-Translator: \n"
"Language-Team: Serbian (Latin)\n"
"Language: sr_CS\n"
@@ -19,44 +19,44 @@ msgstr ""
#: documents/apps.py:10
msgid "Documents"
msgstr ""
msgstr "Dokumenta"
#: documents/models.py:32
msgid "Any word"
msgstr ""
msgstr "Bilo koja reč"
#: documents/models.py:33
msgid "All words"
msgstr ""
msgstr "Sve reči"
#: documents/models.py:34
msgid "Exact match"
msgstr ""
msgstr "Tačno podudaranje"
#: documents/models.py:35
msgid "Regular expression"
msgstr ""
msgstr "Regularni izraz"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr ""
msgstr "Fuzzy reč"
#: documents/models.py:37
msgid "Automatic"
msgstr ""
msgstr "Automatski"
#: documents/models.py:40 documents/models.py:314 paperless_mail/models.py:23
#: paperless_mail/models.py:107
msgid "name"
msgstr ""
msgstr "naziv"
#: documents/models.py:42
msgid "match"
msgstr ""
msgstr "poklapanje"
#: documents/models.py:45
msgid "matching algorithm"
msgstr ""
msgstr "algoritam podudaranja"
#: documents/models.py:48
msgid "is insensitive"
@@ -64,19 +64,19 @@ msgstr ""
#: documents/models.py:61 documents/models.py:104
msgid "correspondent"
msgstr ""
msgstr "dopisnik"
#: documents/models.py:62
msgid "correspondents"
msgstr ""
msgstr "dopisnici"
#: documents/models.py:67
msgid "color"
msgstr ""
msgstr "boja"
#: documents/models.py:70
msgid "is inbox tag"
msgstr ""
msgstr "je oznaka prijemnog sandučeta"
#: documents/models.py:73
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
@@ -84,19 +84,19 @@ msgstr ""
#: documents/models.py:79
msgid "tag"
msgstr ""
msgstr "oznaka"
#: documents/models.py:80 documents/models.py:130
msgid "tags"
msgstr ""
msgstr "oznake"
#: documents/models.py:85 documents/models.py:115
msgid "document type"
msgstr ""
msgstr "tip dokumenta"
#: documents/models.py:86
msgid "document types"
msgstr ""
msgstr "tipovi dokumenta"
#: documents/models.py:94
msgid "Unencrypted"
@@ -108,11 +108,11 @@ msgstr ""
#: documents/models.py:107
msgid "title"
msgstr ""
msgstr "naslov"
#: documents/models.py:119
msgid "content"
msgstr ""
msgstr "sadržaj"
#: documents/models.py:122
msgid "The raw, text-only data of the document. This field is primarily used for searching."
@@ -120,43 +120,43 @@ msgstr ""
#: documents/models.py:127
msgid "mime type"
msgstr ""
msgstr "mime tip"
#: documents/models.py:134
msgid "checksum"
msgstr ""
msgstr "kontrolna suma"
#: documents/models.py:138
msgid "The checksum of the original document."
msgstr ""
msgstr "Kontrolna suma originalnog dokumenta."
#: documents/models.py:142
msgid "archive checksum"
msgstr ""
msgstr "arhivni checksum"
#: documents/models.py:147
msgid "The checksum of the archived document."
msgstr ""
msgstr "Kontrolna suma arhivnog dokumenta."
#: documents/models.py:150 documents/models.py:295
msgid "created"
msgstr ""
msgstr "kreirano"
#: documents/models.py:153
msgid "modified"
msgstr ""
msgstr "izmenjeno"
#: documents/models.py:157
msgid "storage type"
msgstr ""
msgstr "tip skladišta"
#: documents/models.py:165
msgid "added"
msgstr ""
msgstr "dodato"
#: documents/models.py:169
msgid "filename"
msgstr ""
msgstr "naziv fajla"
#: documents/models.py:175
msgid "Current filename in storage"
@@ -164,7 +164,7 @@ msgstr ""
#: documents/models.py:179
msgid "archive filename"
msgstr ""
msgstr "naziv fajla arhive"
#: documents/models.py:185
msgid "Current archive filename in storage"
@@ -172,7 +172,7 @@ msgstr ""
#: documents/models.py:189
msgid "archive serial number"
msgstr ""
msgstr "arhivski serijski broj"
#: documents/models.py:195
msgid "The position of this document in your physical document archive."
@@ -180,75 +180,75 @@ msgstr ""
#: documents/models.py:201
msgid "document"
msgstr ""
msgstr "dokument"
#: documents/models.py:202
msgid "documents"
msgstr ""
msgstr "dokumenta"
#: documents/models.py:280
msgid "debug"
msgstr ""
msgstr "okloni greške"
#: documents/models.py:281
msgid "information"
msgstr ""
msgstr "informacija"
#: documents/models.py:282
msgid "warning"
msgstr ""
msgstr "upozorenje"
#: documents/models.py:283
msgid "error"
msgstr ""
msgstr "grеška"
#: documents/models.py:284
msgid "critical"
msgstr ""
msgstr "kritično"
#: documents/models.py:287
msgid "group"
msgstr ""
msgstr "grupa"
#: documents/models.py:289
msgid "message"
msgstr ""
msgstr "poruka"
#: documents/models.py:292
msgid "level"
msgstr ""
msgstr "nivo"
#: documents/models.py:299
msgid "log"
msgstr ""
msgstr "log"
#: documents/models.py:300
msgid "logs"
msgstr ""
msgstr "logovi"
#: documents/models.py:310 documents/models.py:360
msgid "saved view"
msgstr ""
msgstr "sačuvani prikaz"
#: documents/models.py:311
msgid "saved views"
msgstr ""
msgstr "sačuvani prikazi"
#: documents/models.py:313
msgid "user"
msgstr ""
msgstr "korisnik"
#: documents/models.py:317
msgid "show on dashboard"
msgstr ""
msgstr "prikaži na kontrolnoj tabli"
#: documents/models.py:320
msgid "show in sidebar"
msgstr ""
msgstr "prikaži u bočnoj traci"
#: documents/models.py:324
msgid "sort field"
msgstr ""
msgstr "polje za sortiranje"
#: documents/models.py:326
msgid "sort reverse"
@@ -256,83 +256,83 @@ msgstr ""
#: documents/models.py:331
msgid "title contains"
msgstr ""
msgstr "naslov sadrži"
#: documents/models.py:332
msgid "content contains"
msgstr ""
msgstr "sadržaj sadrži"
#: documents/models.py:333
msgid "ASN is"
msgstr ""
msgstr "ASN je"
#: documents/models.py:334
msgid "correspondent is"
msgstr ""
msgstr "dopisnik je"
#: documents/models.py:335
msgid "document type is"
msgstr ""
msgstr "tip dokumenta je"
#: documents/models.py:336
msgid "is in inbox"
msgstr ""
msgstr "je u prijemnog sandučetu"
#: documents/models.py:337
msgid "has tag"
msgstr ""
msgstr "ima oznaku"
#: documents/models.py:338
msgid "has any tag"
msgstr ""
msgstr "ima bilo koju oznaku"
#: documents/models.py:339
msgid "created before"
msgstr ""
msgstr "kreiran pre"
#: documents/models.py:340
msgid "created after"
msgstr ""
msgstr "kreiran posle"
#: documents/models.py:341
msgid "created year is"
msgstr ""
msgstr "godina kreiranja je"
#: documents/models.py:342
msgid "created month is"
msgstr ""
msgstr "mesec kreiranja je"
#: documents/models.py:343
msgid "created day is"
msgstr ""
msgstr "dan kreiranja je"
#: documents/models.py:344
msgid "added before"
msgstr ""
msgstr "dodat pre"
#: documents/models.py:345
msgid "added after"
msgstr ""
msgstr "dodat posle"
#: documents/models.py:346
msgid "modified before"
msgstr ""
msgstr "izmenjen pre"
#: documents/models.py:347
msgid "modified after"
msgstr ""
msgstr "izmenjen posle"
#: documents/models.py:348
msgid "does not have tag"
msgstr ""
msgstr "nema oznaku"
#: documents/models.py:349
msgid "does not have ASN"
msgstr ""
msgstr "nema ASN"
#: documents/models.py:350
msgid "title or content contains"
msgstr ""
msgstr "naslov i sadržaj sadrži"
#: documents/models.py:351
msgid "fulltext query"
@@ -340,19 +340,19 @@ msgstr ""
#: documents/models.py:352
msgid "more like this"
msgstr ""
msgstr "više ovakvih"
#: documents/models.py:353
msgid "has tags in"
msgstr ""
msgstr "ima oznake u"
#: documents/models.py:363
msgid "rule type"
msgstr ""
msgstr "tip pravila"
#: documents/models.py:365
msgid "value"
msgstr ""
msgstr "vrednost"
#: documents/models.py:368
msgid "filter rule"
@@ -390,7 +390,7 @@ msgstr ""
#: documents/templates/registration/logged_out.html:60
msgid "Sign in again"
msgstr ""
msgstr "Prijavitе sе ponovo"
#: documents/templates/registration/login.html:15
msgid "Paperless-ngx sign in"
@@ -398,7 +398,7 @@ msgstr ""
#: documents/templates/registration/login.html:61
msgid "Please sign in."
msgstr ""
msgstr "Prijavite se."
#: documents/templates/registration/login.html:64
msgid "Your username and password didn't match. Please try again."
@@ -406,83 +406,83 @@ msgstr ""
#: documents/templates/registration/login.html:67
msgid "Username"
msgstr ""
msgstr "Korisničko ime"
#: documents/templates/registration/login.html:68
msgid "Password"
msgstr ""
msgstr "Lozinka"
#: documents/templates/registration/login.html:73
msgid "Sign in"
msgstr ""
msgstr "Prijavite se"
#: paperless/settings.py:299
msgid "English (US)"
msgstr ""
msgstr "Engleski (US)"
#: paperless/settings.py:300
msgid "Czech"
msgstr ""
msgstr "Češki"
#: paperless/settings.py:301
msgid "Danish"
msgstr ""
msgstr "Danski"
#: paperless/settings.py:302
msgid "German"
msgstr ""
msgstr "Nemački"
#: paperless/settings.py:303
msgid "English (GB)"
msgstr ""
msgstr "Engleski (UK)"
#: paperless/settings.py:304
msgid "Spanish"
msgstr ""
msgstr "Španski"
#: paperless/settings.py:305
msgid "French"
msgstr ""
msgstr "Francuski"
#: paperless/settings.py:306
msgid "Italian"
msgstr ""
msgstr "Italijanski"
#: paperless/settings.py:307
msgid "Luxembourgish"
msgstr ""
msgstr "Luksemburški"
#: paperless/settings.py:308
msgid "Dutch"
msgstr ""
msgstr "Holandski"
#: paperless/settings.py:309
msgid "Polish"
msgstr ""
msgstr "Poljski"
#: paperless/settings.py:310
msgid "Portuguese (Brazil)"
msgstr ""
msgstr "Portugalski (Brazil)"
#: paperless/settings.py:311
msgid "Portuguese"
msgstr ""
msgstr "Portugalski"
#: paperless/settings.py:312
msgid "Romanian"
msgstr ""
msgstr "Rumunski"
#: paperless/settings.py:313
msgid "Russian"
msgstr ""
msgstr "Ruski"
#: paperless/settings.py:314
msgid "Swedish"
msgstr ""
msgstr "Švedski"
#: paperless/urls.py:139
msgid "Paperless-ngx administration"
msgstr ""
msgstr "Paperless-ngx administracija"
#: paperless_mail/admin.py:29
msgid "Authentication"
@@ -490,11 +490,11 @@ msgstr ""
#: paperless_mail/admin.py:30
msgid "Advanced settings"
msgstr ""
msgstr "Napredna podešavanja"
#: paperless_mail/admin.py:47
msgid "Filter"
msgstr ""
msgstr "Filter"
#: paperless_mail/admin.py:50
msgid "Paperless will only process mails that match ALL of the filters given below."
@@ -502,7 +502,7 @@ msgstr ""
#: paperless_mail/admin.py:64
msgid "Actions"
msgstr ""
msgstr "Radnje"
#: paperless_mail/admin.py:67
msgid "The action applied to the mail. This action is only performed when documents were consumed from the mail. Mails without attachments will remain entirely untouched."
@@ -510,7 +510,7 @@ msgstr ""
#: paperless_mail/admin.py:75
msgid "Metadata"
msgstr ""
msgstr "Metapodaci"
#: paperless_mail/admin.py:78
msgid "Assign metadata to documents consumed from this rule automatically. If you do not assign tags, types or correspondents here, paperless will still process all matching rules that you have defined."
@@ -518,15 +518,15 @@ msgstr ""
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr ""
msgstr "Paperless mejl"
#: paperless_mail/models.py:10
msgid "mail account"
msgstr ""
msgstr "mejl nalog"
#: paperless_mail/models.py:11
msgid "mail accounts"
msgstr ""
msgstr "mejl nalozi"
#: paperless_mail/models.py:18
msgid "No encryption"
@@ -534,19 +534,19 @@ msgstr ""
#: paperless_mail/models.py:19
msgid "Use SSL"
msgstr ""
msgstr "Koristi SSL"
#: paperless_mail/models.py:20
msgid "Use STARTTLS"
msgstr ""
msgstr "Koristi STARTTLS"
#: paperless_mail/models.py:25
msgid "IMAP server"
msgstr ""
msgstr "IMAP server"
#: paperless_mail/models.py:28
msgid "IMAP port"
msgstr ""
msgstr "IMAP port"
#: paperless_mail/models.py:32
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
@@ -554,19 +554,19 @@ msgstr ""
#: paperless_mail/models.py:38
msgid "IMAP security"
msgstr ""
msgstr "IMAP bezbednost"
#: paperless_mail/models.py:41
msgid "username"
msgstr ""
msgstr "korisničko ime"
#: paperless_mail/models.py:43
msgid "password"
msgstr ""
msgstr "lozinka"
#: paperless_mail/models.py:46
msgid "character set"
msgstr ""
msgstr "karakter set"
#: paperless_mail/models.py:50
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
@@ -602,7 +602,7 @@ msgstr ""
#: paperless_mail/models.py:84
msgid "Delete"
msgstr ""
msgstr "Obriši"
#: paperless_mail/models.py:91
msgid "Use subject as title"
@@ -614,51 +614,51 @@ msgstr ""
#: paperless_mail/models.py:101
msgid "Do not assign a correspondent"
msgstr ""
msgstr "Ne dodeljuj dopisnika"
#: paperless_mail/models.py:102
msgid "Use mail address"
msgstr ""
msgstr "Koristi mejl adresu"
#: paperless_mail/models.py:103
msgid "Use name (or mail address if not available)"
msgstr ""
msgstr "Koristi naziv (ili mejl adresu ako nije dostupno)"
#: paperless_mail/models.py:104
msgid "Use correspondent selected below"
msgstr ""
msgstr "Koristi dopisnika ispod"
#: paperless_mail/models.py:109
msgid "order"
msgstr ""
msgstr "raspored"
#: paperless_mail/models.py:115
msgid "account"
msgstr ""
msgstr "nalog"
#: paperless_mail/models.py:119
msgid "folder"
msgstr ""
msgstr "folder"
#: paperless_mail/models.py:122
msgid "Subfolders must be separated by dots."
msgstr ""
msgstr "Podfolderi moraju biti odvojeni tačkama."
#: paperless_mail/models.py:126
msgid "filter from"
msgstr ""
msgstr "filter od"
#: paperless_mail/models.py:129
msgid "filter subject"
msgstr ""
msgstr "filter naslov"
#: paperless_mail/models.py:132
msgid "filter body"
msgstr ""
msgstr "filter telo poruke"
#: paperless_mail/models.py:136
msgid "filter attachment filename"
msgstr ""
msgstr "filter naziv fajla priloga"
#: paperless_mail/models.py:141
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
@@ -674,7 +674,7 @@ msgstr ""
#: paperless_mail/models.py:152
msgid "attachment type"
msgstr ""
msgstr "tip priloga"
#: paperless_mail/models.py:156
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
@@ -682,7 +682,7 @@ msgstr ""
#: paperless_mail/models.py:162
msgid "action"
msgstr ""
msgstr "radnja"
#: paperless_mail/models.py:168
msgid "action parameter"
@@ -694,21 +694,21 @@ msgstr ""
#: paperless_mail/models.py:181
msgid "assign title from"
msgstr ""
msgstr "dodeli naziv iz"
#: paperless_mail/models.py:189
msgid "assign this tag"
msgstr ""
msgstr "dodeli ovu oznaku"
#: paperless_mail/models.py:197
msgid "assign this document type"
msgstr ""
msgstr "dodeli ovaj tip dokumenta"
#: paperless_mail/models.py:201
msgid "assign correspondent from"
msgstr ""
msgstr "dodeli dopisnika iz"
#: paperless_mail/models.py:211
msgid "assign this correspondent"
msgstr ""
msgstr "dodeli ovog dopisnika"

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-03-02 11:20-0800\n"
"PO-Revision-Date: 2022-03-09 23:50\n"
"PO-Revision-Date: 2022-03-29 08:58\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -104,7 +104,7 @@ msgstr "未加密"
#: documents/models.py:95
msgid "Encrypted with GNU Privacy Guard"
msgstr "使用 GNU 隐私防护加密"
msgstr "使用 GNU 隐私防护GPG加密"
#: documents/models.py:107
msgid "title"
@@ -308,19 +308,19 @@ msgstr "创建日期是"
#: documents/models.py:344
msgid "added before"
msgstr "添加于"
msgstr "添加于"
#: documents/models.py:345
msgid "added after"
msgstr "添加"
msgstr "添加晚于"
#: documents/models.py:346
msgid "modified before"
msgstr "修改"
msgstr "修改早于"
#: documents/models.py:347
msgid "modified after"
msgstr "修改"
msgstr "修改晚于"
#: documents/models.py:348
msgid "does not have tag"
@@ -344,7 +344,7 @@ msgstr "更多类似内容"
#: documents/models.py:353
msgid "has tags in"
msgstr "有标签于"
msgstr "有标签包含于"
#: documents/models.py:363
msgid "rule type"
@@ -382,11 +382,11 @@ msgstr "Paperless-ngx 正在加载..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ngx signed out"
msgstr "Paperless-ngx 注销"
msgstr "Paperless-ngx 已退出"
#: documents/templates/registration/logged_out.html:59
msgid "You have been successfully logged out. Bye!"
msgstr "您已成功注销。Bye"
msgstr "您已成功退出。再见"
#: documents/templates/registration/logged_out.html:60
msgid "Sign in again"
@@ -402,7 +402,7 @@ msgstr "请登录。"
#: documents/templates/registration/login.html:64
msgid "Your username and password didn't match. Please try again."
msgstr "您的用户名和密码不匹配请重试。"
msgstr "您的用户名和密码不匹配请重试。"
#: documents/templates/registration/login.html:67
msgid "Username"
@@ -486,7 +486,7 @@ msgstr "Paperless-ngx 管理"
#: paperless_mail/admin.py:29
msgid "Authentication"
msgstr "证"
msgstr "身份验证"
#: paperless_mail/admin.py:30
msgid "Advanced settings"
@@ -506,7 +506,7 @@ msgstr "操作"
#: paperless_mail/admin.py:67
msgid "The action applied to the mail. This action is only performed when documents were consumed from the mail. Mails without attachments will remain entirely untouched."
msgstr "应用于邮件的操作。此操作仅在从邮件中处理文档时执行。没有附件的邮件将保持完全不受接触。"
msgstr "应用于邮件的操作。此操作仅在从邮件中处理文档时执行。没有附件的邮件完全不会触及。"
#: paperless_mail/admin.py:75
msgid "Metadata"
@@ -514,7 +514,7 @@ msgstr "元数据"
#: paperless_mail/admin.py:78
msgid "Assign metadata to documents consumed from this rule automatically. If you do not assign tags, types or correspondents here, paperless will still process all matching rules that you have defined."
msgstr "将元数据自动分配到从此规则处理的文档。 如果您不在这里分配标签、类型或联系人Paperless-ngx 仍将处理您已定义的所有匹配规则。"
msgstr "将元数据自动指定到被此规则处理的文档。 如果您不在这里指定标签、类型或联系人Paperless-ngx 仍将处理您已定义的所有匹配规则。"
#: paperless_mail/apps.py:9
msgid "Paperless mail"
@@ -522,7 +522,7 @@ msgstr "Paperless-ngx 邮件"
#: paperless_mail/models.py:10
msgid "mail account"
msgstr "邮件帐户"
msgstr "邮件账号"
#: paperless_mail/models.py:11
msgid "mail accounts"
@@ -570,7 +570,7 @@ msgstr "字符集"
#: paperless_mail/models.py:50
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr "与邮件服务器通信时使用的字符集,例如'UTF-8'或 'US-ASCII'。"
msgstr "与邮件服务器通信时使用的字符集,例如UTF-8”或“US-ASCII。"
#: paperless_mail/models.py:61
msgid "mail rule"
@@ -594,7 +594,7 @@ msgstr "标记为已读,不处理已读邮件"
#: paperless_mail/models.py:82
msgid "Flag the mail, don't process flagged mails"
msgstr "标记邮件,不处理标记的邮件"
msgstr "标记邮件,不处理标记的邮件"
#: paperless_mail/models.py:83
msgid "Move to specified folder"
@@ -634,7 +634,7 @@ msgstr "排序"
#: paperless_mail/models.py:115
msgid "account"
msgstr "户"
msgstr "户"
#: paperless_mail/models.py:119
msgid "folder"
@@ -642,7 +642,7 @@ msgstr "文件夹"
#: paperless_mail/models.py:122
msgid "Subfolders must be separated by dots."
msgstr "子文件夹必须用 \".\" 分隔。"
msgstr "子文件夹必须用“.”分隔。"
#: paperless_mail/models.py:126
msgid "filter from"
@@ -662,7 +662,7 @@ msgstr "过滤附件文件名"
#: paperless_mail/models.py:141
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "如果指定的话,只处理完全匹配此文件名的文档。允许使用通配符,如 *.pdf 或 *发票*。不区分大小写。"
msgstr "如果指定了文件名,只处理完全匹配此文件名的文档。允许使用通配符,如 *.pdf 或 *发票*。不区分大小写。"
#: paperless_mail/models.py:148
msgid "maximum age"
@@ -690,11 +690,11 @@ msgstr "操作参数"
#: paperless_mail/models.py:173
msgid "Additional parameter for the action selected above, i.e., the target folder of the move to folder action. Subfolders must be separated by dots."
msgstr "上面选择的操作的附加参数,即移动到文件夹操作的目标文件夹。子文件夹必须用 \".\" 来分隔。"
msgstr "上面选择的操作的附加参数,即移动到文件夹操作的目标文件夹。子文件夹必须用“.”来分隔。"
#: paperless_mail/models.py:181
msgid "assign title from"
msgstr "分配标题自"
msgstr "分配标题自"
#: paperless_mail/models.py:189
msgid "assign this tag"
@@ -706,7 +706,7 @@ msgstr "分配此文档类型"
#: paperless_mail/models.py:201
msgid "assign correspondent from"
msgstr "分配联系人自"
msgstr "分配联系人自"
#: paperless_mail/models.py:211
msgid "assign this correspondent"

View File

@@ -3,6 +3,8 @@ import math
import multiprocessing
import os
import re
from typing import Final
from urllib.parse import urlparse
from concurrent_log_handler.queue import setup_logging_queues
from django.utils.translation import gettext_lazy as _
@@ -29,7 +31,7 @@ elif os.path.exists("/usr/local/etc/paperless.conf"):
os.environ["OMP_THREAD_LIMIT"] = "1"
def __get_boolean(key, default="NO"):
def __get_boolean(key: str, default: str = "NO") -> bool:
"""
Return a boolean value based on whatever the user has supplied in the
environment based on whether the value "looks like" it's True or not.
@@ -37,6 +39,13 @@ def __get_boolean(key, default="NO"):
return bool(os.getenv(key, default).lower() in ("yes", "y", "1", "t", "true"))
def __get_int(key: str, default: int) -> int:
"""
Return an integer value based on the environment variable or a default
"""
return int(os.getenv(key, default))
# NEVER RUN WITH DEBUG IN PRODUCTION.
DEBUG = __get_boolean("PAPERLESS_DEBUG", "NO")
@@ -211,7 +220,15 @@ if DEBUG:
else:
X_FRAME_OPTIONS = "SAMEORIGIN"
# We allow CORS from localhost:8080
# The next 3 settings can also be set using just PAPERLESS_URL
_csrf_origins = os.getenv("PAPERLESS_CSRF_TRUSTED_ORIGINS")
if _csrf_origins:
CSRF_TRUSTED_ORIGINS = _csrf_origins.split(",")
else:
CSRF_TRUSTED_ORIGINS = []
# We allow CORS from localhost:8000
CORS_ALLOWED_ORIGINS = tuple(
os.getenv("PAPERLESS_CORS_ALLOWED_HOSTS", "http://localhost:8000").split(","),
)
@@ -220,6 +237,22 @@ if DEBUG:
# Allow access from the angular development server during debugging
CORS_ALLOWED_ORIGINS += ("http://localhost:4200",)
_allowed_hosts = os.getenv("PAPERLESS_ALLOWED_HOSTS")
if _allowed_hosts:
ALLOWED_HOSTS = _allowed_hosts.split(",")
else:
ALLOWED_HOSTS = ["*"]
_paperless_url = os.getenv("PAPERLESS_URL")
if _paperless_url:
_paperless_uri = urlparse(_paperless_url)
CSRF_TRUSTED_ORIGINS.append(_paperless_url)
CORS_ALLOWED_ORIGINS += (_paperless_url,)
if _allowed_hosts:
ALLOWED_HOSTS.append(_paperless_uri.hostname)
else:
ALLOWED_HOSTS = [_paperless_uri.hostname]
# The secret key has a default that should be fine so long as you're hosting
# Paperless on a closed network. However, if you're putting this anywhere
# public, you should change the key to something unique and verbose.
@@ -228,12 +261,6 @@ SECRET_KEY = os.getenv(
"e11fl1oa-*ytql8p)(06fbj4ukrlo+n7k&q5+$1md7i+mge=ee",
)
_allowed_hosts = os.getenv("PAPERLESS_ALLOWED_HOSTS")
if _allowed_hosts:
ALLOWED_HOSTS = _allowed_hosts.split(",")
else:
ALLOWED_HOSTS = ["*"]
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
@@ -299,6 +326,7 @@ LANGUAGE_CODE = "en-us"
LANGUAGES = [
("en-us", _("English (US)")), # needs to be first to act as fallback language
("be-by", _("Belarusian")),
("cs-cz", _("Czech")),
("da-dk", _("Danish")),
("de-de", _("German")),
@@ -395,7 +423,7 @@ LOGGING = {
# in total.
def default_task_workers():
def default_task_workers() -> int:
# always leave one core open
available_cores = max(multiprocessing.cpu_count(), 1)
try:
@@ -406,20 +434,29 @@ def default_task_workers():
return 1
TASK_WORKERS = int(os.getenv("PAPERLESS_TASK_WORKERS", default_task_workers()))
TASK_WORKERS = __get_int("PAPERLESS_TASK_WORKERS", default_task_workers())
PAPERLESS_WORKER_TIMEOUT: Final[int] = __get_int("PAPERLESS_WORKER_TIMEOUT", 1800)
# Per django-q docs, timeout must be smaller than retry
# We default retry to 10s more than the timeout
PAPERLESS_WORKER_RETRY: Final[int] = __get_int(
"PAPERLESS_WORKER_RETRY",
PAPERLESS_WORKER_TIMEOUT + 10,
)
Q_CLUSTER = {
"name": "paperless",
"catch_up": False,
"recycle": 1,
"retry": 1800,
"timeout": int(os.getenv("PAPERLESS_WORKER_TIMEOUT", 1800)),
"retry": PAPERLESS_WORKER_RETRY,
"timeout": PAPERLESS_WORKER_TIMEOUT,
"workers": TASK_WORKERS,
"redis": os.getenv("PAPERLESS_REDIS", "redis://localhost:6379"),
}
def default_threads_per_worker(task_workers):
def default_threads_per_worker(task_workers) -> int:
# always leave one core open
available_cores = max(multiprocessing.cpu_count(), 1)
try:
@@ -454,13 +491,19 @@ CONSUMER_IGNORE_PATTERNS = list(
json.loads(
os.getenv(
"PAPERLESS_CONSUMER_IGNORE_PATTERNS",
'[".DS_STORE/*", "._*", ".stfolder/*"]',
'[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"]',
),
),
)
CONSUMER_SUBDIRS_AS_TAGS = __get_boolean("PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS")
CONSUMER_ENABLE_BARCODES = __get_boolean(
"PAPERLESS_CONSUMER_ENABLE_BARCODES",
)
CONSUMER_BARCODE_STRING = os.getenv("PAPERLESS_CONSUMER_BARCODE_STRING", "PATCHT")
OPTIMIZE_THUMBNAILS = __get_boolean("PAPERLESS_OPTIMIZE_THUMBNAILS", "true")
OCR_PAGES = int(os.getenv("PAPERLESS_OCR_PAGES", 0))
@@ -489,6 +532,11 @@ OCR_ROTATE_PAGES_THRESHOLD = float(
os.getenv("PAPERLESS_OCR_ROTATE_PAGES_THRESHOLD", 12.0),
)
OCR_MAX_IMAGE_PIXELS = os.environ.get(
"PAPERLESS_OCR_MAX_IMAGE_PIXELS",
256000000,
)
OCR_USER_ARGS = os.getenv("PAPERLESS_OCR_USER_ARGS", "{}")
# GNUPG needs a home directory for some reason
@@ -560,3 +608,7 @@ if os.getenv("PAPERLESS_IGNORE_DATES", ""):
d = dateparser.parse(s)
if d:
IGNORE_DATES.add(d.date())
ENABLE_UPDATE_CHECK = os.getenv("PAPERLESS_ENABLE_UPDATE_CHECK", "default")
if ENABLE_UPDATE_CHECK != "default":
ENABLE_UPDATE_CHECK = __get_boolean("PAPERLESS_ENABLE_UPDATE_CHECK")

View File

@@ -14,6 +14,7 @@ from documents.views import DocumentTypeViewSet
from documents.views import IndexView
from documents.views import LogViewSet
from documents.views import PostDocumentView
from documents.views import RemoteVersionView
from documents.views import SavedViewViewSet
from documents.views import SearchAutoCompleteView
from documents.views import SelectionDataView
@@ -72,6 +73,11 @@ urlpatterns = [
BulkDownloadView.as_view(),
name="bulk_download",
),
re_path(
r"^remote_version/",
RemoteVersionView.as_view(),
name="remoteversion",
),
path("token/", views.obtain_auth_token),
]
+ api_router.urls,

View File

@@ -62,13 +62,13 @@ class FlagMailAction(BaseMailAction):
def get_rule_action(rule):
if rule.action == MailRule.ACTION_FLAG:
if rule.action == MailRule.AttachmentAction.FLAG:
return FlagMailAction()
elif rule.action == MailRule.ACTION_DELETE:
elif rule.action == MailRule.AttachmentAction.DELETE:
return DeleteMailAction()
elif rule.action == MailRule.ACTION_MOVE:
elif rule.action == MailRule.AttachmentAction.MOVE:
return MoveMailAction()
elif rule.action == MailRule.ACTION_MARK_READ:
elif rule.action == MailRule.AttachmentAction.MARK_READ:
return MarkReadMailAction()
else:
raise NotImplementedError("Unknown action.") # pragma: nocover
@@ -117,10 +117,10 @@ class MailAccountHandler(LoggingMixin):
return None
def get_title(self, message, att, rule):
if rule.assign_title_from == MailRule.TITLE_FROM_SUBJECT:
if rule.assign_title_from == MailRule.TitleSource.FROM_SUBJECT:
return message.subject
elif rule.assign_title_from == MailRule.TITLE_FROM_FILENAME:
elif rule.assign_title_from == MailRule.TitleSource.FROM_FILENAME:
return os.path.splitext(os.path.basename(att.filename))[0]
else:
@@ -131,20 +131,20 @@ class MailAccountHandler(LoggingMixin):
def get_correspondent(self, message: MailMessage, rule):
c_from = rule.assign_correspondent_from
if c_from == MailRule.CORRESPONDENT_FROM_NOTHING:
if c_from == MailRule.CorrespondentSource.FROM_NOTHING:
return None
elif c_from == MailRule.CORRESPONDENT_FROM_EMAIL:
elif c_from == MailRule.CorrespondentSource.FROM_EMAIL:
return self._correspondent_from_name(message.from_)
elif c_from == MailRule.CORRESPONDENT_FROM_NAME:
elif c_from == MailRule.CorrespondentSource.FROM_NAME:
from_values = message.from_values
if from_values is not None and len(from_values.name) > 0:
return self._correspondent_from_name(from_values.name)
else:
return self._correspondent_from_name(message.from_)
elif c_from == MailRule.CORRESPONDENT_FROM_CUSTOM:
elif c_from == MailRule.CorrespondentSource.FROM_CUSTOM:
return rule.assign_correspondent
else:
@@ -273,7 +273,7 @@ class MailAccountHandler(LoggingMixin):
return total_processed_files
def handle_message(self, message, rule):
def handle_message(self, message, rule) -> int:
if not message.attachments:
return 0
@@ -294,7 +294,8 @@ class MailAccountHandler(LoggingMixin):
if (
not att.content_disposition == "attachment"
and rule.attachment_type == MailRule.ATTACHMENT_TYPE_ATTACHMENTS_ONLY
and rule.attachment_type
== MailRule.AttachmentProcessing.ATTACHMENTS_ONLY
):
self.log(
"debug",
@@ -305,7 +306,12 @@ class MailAccountHandler(LoggingMixin):
continue
if rule.filter_attachment_filename:
if not fnmatch(att.filename, rule.filter_attachment_filename):
# Force the filename and pattern to the lowercase
# as this is system dependent otherwise
if not fnmatch(
att.filename.lower(),
rule.filter_attachment_filename.lower(),
):
continue
title = self.get_title(message, att, rule)

View File

@@ -0,0 +1,37 @@
# Generated by Django 4.0.3 on 2022-03-28 17:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("paperless_mail", "0008_auto_20210516_0940"),
]
operations = [
migrations.AlterField(
model_name="mailrule",
name="action",
field=models.PositiveIntegerField(
choices=[
(1, "Mark as read, don't process read mails"),
(2, "Flag the mail, don't process flagged mails"),
(3, "Move to specified folder"),
(4, "Delete"),
],
default=3,
verbose_name="action",
),
),
migrations.AlterField(
model_name="mailrule",
name="folder",
field=models.CharField(
default="INBOX",
help_text="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server.",
max_length=256,
verbose_name="folder",
),
),
]

View File

@@ -8,15 +8,10 @@ class MailAccount(models.Model):
verbose_name = _("mail account")
verbose_name_plural = _("mail accounts")
IMAP_SECURITY_NONE = 1
IMAP_SECURITY_SSL = 2
IMAP_SECURITY_STARTTLS = 3
IMAP_SECURITY_OPTIONS = (
(IMAP_SECURITY_NONE, _("No encryption")),
(IMAP_SECURITY_SSL, _("Use SSL")),
(IMAP_SECURITY_STARTTLS, _("Use STARTTLS")),
)
class ImapSecurity(models.IntegerChoices):
NONE = 1, _("No encryption")
SSL = 2, _("Use SSL")
STARTTLS = 3, _("Use STARTTLS")
name = models.CharField(_("name"), max_length=256, unique=True)
@@ -34,8 +29,8 @@ class MailAccount(models.Model):
imap_security = models.PositiveIntegerField(
_("IMAP security"),
choices=IMAP_SECURITY_OPTIONS,
default=IMAP_SECURITY_SSL,
choices=ImapSecurity.choices,
default=ImapSecurity.SSL,
)
username = models.CharField(_("username"), max_length=256)
@@ -61,48 +56,25 @@ class MailRule(models.Model):
verbose_name = _("mail rule")
verbose_name_plural = _("mail rules")
ATTACHMENT_TYPE_ATTACHMENTS_ONLY = 1
ATTACHMENT_TYPE_EVERYTHING = 2
class AttachmentProcessing(models.IntegerChoices):
ATTACHMENTS_ONLY = 1, _("Only process attachments.")
EVERYTHING = 2, _("Process all files, including 'inline' " "attachments.")
ATTACHMENT_TYPES = (
(ATTACHMENT_TYPE_ATTACHMENTS_ONLY, _("Only process attachments.")),
(
ATTACHMENT_TYPE_EVERYTHING,
_("Process all files, including 'inline' " "attachments."),
),
)
class AttachmentAction(models.IntegerChoices):
DELETE = 1, _("Mark as read, don't process read mails")
MOVE = 2, _("Flag the mail, don't process flagged mails")
MARK_READ = 3, _("Move to specified folder")
FLAG = 4, _("Delete")
ACTION_DELETE = 1
ACTION_MOVE = 2
ACTION_MARK_READ = 3
ACTION_FLAG = 4
class TitleSource(models.IntegerChoices):
FROM_SUBJECT = 1, _("Use subject as title")
FROM_FILENAME = 2, _("Use attachment filename as title")
ACTIONS = (
(ACTION_MARK_READ, _("Mark as read, don't process read mails")),
(ACTION_FLAG, _("Flag the mail, don't process flagged mails")),
(ACTION_MOVE, _("Move to specified folder")),
(ACTION_DELETE, _("Delete")),
)
TITLE_FROM_SUBJECT = 1
TITLE_FROM_FILENAME = 2
TITLE_SELECTOR = (
(TITLE_FROM_SUBJECT, _("Use subject as title")),
(TITLE_FROM_FILENAME, _("Use attachment filename as title")),
)
CORRESPONDENT_FROM_NOTHING = 1
CORRESPONDENT_FROM_EMAIL = 2
CORRESPONDENT_FROM_NAME = 3
CORRESPONDENT_FROM_CUSTOM = 4
CORRESPONDENT_SELECTOR = (
(CORRESPONDENT_FROM_NOTHING, _("Do not assign a correspondent")),
(CORRESPONDENT_FROM_EMAIL, _("Use mail address")),
(CORRESPONDENT_FROM_NAME, _("Use name (or mail address if not available)")),
(CORRESPONDENT_FROM_CUSTOM, _("Use correspondent selected below")),
)
class CorrespondentSource(models.IntegerChoices):
FROM_NOTHING = 1, _("Do not assign a correspondent")
FROM_EMAIL = 2, _("Use mail address")
FROM_NAME = 3, _("Use name (or mail address if not available)")
FROM_CUSTOM = 4, _("Use correspondent selected below")
name = models.CharField(_("name"), max_length=256, unique=True)
@@ -119,7 +91,10 @@ class MailRule(models.Model):
_("folder"),
default="INBOX",
max_length=256,
help_text=_("Subfolders must be separated by dots."),
help_text=_(
"Subfolders must be separated by a delimiter, often a dot ('.') or"
" slash ('/'), but it varies by mail server.",
),
)
filter_from = models.CharField(
@@ -161,8 +136,8 @@ class MailRule(models.Model):
attachment_type = models.PositiveIntegerField(
_("attachment type"),
choices=ATTACHMENT_TYPES,
default=ATTACHMENT_TYPE_ATTACHMENTS_ONLY,
choices=AttachmentProcessing.choices,
default=AttachmentProcessing.ATTACHMENTS_ONLY,
help_text=_(
"Inline attachments include embedded images, so it's best "
"to combine this option with a filename filter.",
@@ -171,8 +146,8 @@ class MailRule(models.Model):
action = models.PositiveIntegerField(
_("action"),
choices=ACTIONS,
default=ACTION_MARK_READ,
choices=AttachmentAction.choices,
default=AttachmentAction.MARK_READ,
)
action_parameter = models.CharField(
@@ -190,8 +165,8 @@ class MailRule(models.Model):
assign_title_from = models.PositiveIntegerField(
_("assign title from"),
choices=TITLE_SELECTOR,
default=TITLE_FROM_SUBJECT,
choices=TitleSource.choices,
default=TitleSource.FROM_SUBJECT,
)
assign_tag = models.ForeignKey(
@@ -212,8 +187,8 @@ class MailRule(models.Model):
assign_correspondent_from = models.PositiveIntegerField(
_("assign correspondent from"),
choices=CORRESPONDENT_SELECTOR,
default=CORRESPONDENT_FROM_NOTHING,
choices=CorrespondentSource.choices,
default=CorrespondentSource.FROM_NOTHING,
)
assign_correspondent = models.ForeignKey(

View File

@@ -246,13 +246,13 @@ class TestMail(DirectoriesMixin, TestCase):
rule = MailRule(
name="a",
assign_correspondent_from=MailRule.CORRESPONDENT_FROM_NOTHING,
assign_correspondent_from=MailRule.CorrespondentSource.FROM_NOTHING,
)
self.assertIsNone(handler.get_correspondent(message, rule))
rule = MailRule(
name="b",
assign_correspondent_from=MailRule.CORRESPONDENT_FROM_EMAIL,
assign_correspondent_from=MailRule.CorrespondentSource.FROM_EMAIL,
)
c = handler.get_correspondent(message, rule)
self.assertIsNotNone(c)
@@ -264,7 +264,7 @@ class TestMail(DirectoriesMixin, TestCase):
rule = MailRule(
name="c",
assign_correspondent_from=MailRule.CORRESPONDENT_FROM_NAME,
assign_correspondent_from=MailRule.CorrespondentSource.FROM_NAME,
)
c = handler.get_correspondent(message, rule)
self.assertIsNotNone(c)
@@ -275,7 +275,7 @@ class TestMail(DirectoriesMixin, TestCase):
rule = MailRule(
name="d",
assign_correspondent_from=MailRule.CORRESPONDENT_FROM_CUSTOM,
assign_correspondent_from=MailRule.CorrespondentSource.FROM_CUSTOM,
assign_correspondent=someone_else,
)
c = handler.get_correspondent(message, rule)
@@ -289,9 +289,15 @@ class TestMail(DirectoriesMixin, TestCase):
handler = MailAccountHandler()
rule = MailRule(name="a", assign_title_from=MailRule.TITLE_FROM_FILENAME)
rule = MailRule(
name="a",
assign_title_from=MailRule.TitleSource.FROM_FILENAME,
)
self.assertEqual(handler.get_title(message, att, rule), "this_is_the_file")
rule = MailRule(name="b", assign_title_from=MailRule.TITLE_FROM_SUBJECT)
rule = MailRule(
name="b",
assign_title_from=MailRule.TitleSource.FROM_SUBJECT,
)
self.assertEqual(handler.get_title(message, att, rule), "the message title")
def test_handle_message(self):
@@ -302,7 +308,10 @@ class TestMail(DirectoriesMixin, TestCase):
)
account = MailAccount()
rule = MailRule(assign_title_from=MailRule.TITLE_FROM_FILENAME, account=account)
rule = MailRule(
assign_title_from=MailRule.TitleSource.FROM_FILENAME,
account=account,
)
result = self.mail_account_handler.handle_message(message, rule)
@@ -346,7 +355,10 @@ class TestMail(DirectoriesMixin, TestCase):
)
account = MailAccount()
rule = MailRule(assign_title_from=MailRule.TITLE_FROM_FILENAME, account=account)
rule = MailRule(
assign_title_from=MailRule.TitleSource.FROM_FILENAME,
account=account,
)
result = self.mail_account_handler.handle_message(message, rule)
@@ -369,7 +381,10 @@ class TestMail(DirectoriesMixin, TestCase):
)
account = MailAccount()
rule = MailRule(assign_title_from=MailRule.TITLE_FROM_FILENAME, account=account)
rule = MailRule(
assign_title_from=MailRule.TitleSource.FROM_FILENAME,
account=account,
)
result = self.mail_account_handler.handle_message(message, rule)
@@ -392,9 +407,9 @@ class TestMail(DirectoriesMixin, TestCase):
account = MailAccount()
rule = MailRule(
assign_title_from=MailRule.TITLE_FROM_FILENAME,
assign_title_from=MailRule.TitleSource.FROM_FILENAME,
account=account,
attachment_type=MailRule.ATTACHMENT_TYPE_EVERYTHING,
attachment_type=MailRule.AttachmentProcessing.EVERYTHING,
)
result = self.mail_account_handler.handle_message(message, rule)
@@ -409,33 +424,36 @@ class TestMail(DirectoriesMixin, TestCase):
_AttachmentDef(filename="f2.pdf"),
_AttachmentDef(filename="f3.pdf"),
_AttachmentDef(filename="f2.png"),
_AttachmentDef(filename="file.PDf"),
_AttachmentDef(filename="f1.Pdf"),
],
)
tests = [
("*.pdf", ["f1.pdf", "f2.pdf", "f3.pdf"]),
("f1.pdf", ["f1.pdf"]),
("*.pdf", ["f1.pdf", "f1.Pdf", "f2.pdf", "f3.pdf", "file.PDf"]),
("f1.pdf", ["f1.pdf", "f1.Pdf"]),
("f1", []),
("*", ["f1.pdf", "f2.pdf", "f3.pdf", "f2.png"]),
("*", ["f1.pdf", "f2.pdf", "f3.pdf", "f2.png", "f1.Pdf", "file.PDf"]),
("*.png", ["f2.png"]),
]
for (pattern, matches) in tests:
matches.sort()
self.async_task.reset_mock()
account = MailAccount()
rule = MailRule(
assign_title_from=MailRule.TITLE_FROM_FILENAME,
assign_title_from=MailRule.TitleSource.FROM_FILENAME,
account=account,
filter_attachment_filename=pattern,
)
result = self.mail_account_handler.handle_message(message, rule)
self.assertEqual(result, len(matches))
filenames = [
a[1]["override_filename"] for a in self.async_task.call_args_list
]
self.assertCountEqual(filenames, matches)
self.assertEqual(result, len(matches), f"Error with pattern: {pattern}")
filenames = sorted(
[a[1]["override_filename"] for a in self.async_task.call_args_list],
)
self.assertListEqual(filenames, matches)
def test_handle_mail_account_mark_read(self):
@@ -449,7 +467,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_MARK_READ,
action=MailRule.AttachmentAction.MARK_READ,
)
self.assertEqual(len(self.bogus_mailbox.messages), 3)
@@ -472,7 +490,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_DELETE,
action=MailRule.AttachmentAction.DELETE,
filter_subject="Invoice",
)
@@ -493,7 +511,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_FLAG,
action=MailRule.AttachmentAction.FLAG,
filter_subject="Invoice",
)
@@ -516,7 +534,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_MOVE,
action=MailRule.AttachmentAction.MOVE,
action_parameter="spam",
filter_subject="Claim",
)
@@ -562,7 +580,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_MOVE,
action=MailRule.AttachmentAction.MOVE,
action_parameter="spam",
filter_subject="Claim",
)
@@ -583,7 +601,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_MOVE,
action=MailRule.AttachmentAction.MOVE,
action_parameter="spam",
filter_subject="Claim",
order=1,
@@ -592,7 +610,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule2",
account=account,
action=MailRule.ACTION_MOVE,
action=MailRule.AttachmentAction.MOVE,
action_parameter="spam",
filter_subject="Claim",
order=2,
@@ -622,7 +640,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_MOVE,
action=MailRule.AttachmentAction.MOVE,
action_parameter="spam",
)
@@ -647,9 +665,9 @@ class TestMail(DirectoriesMixin, TestCase):
name="testrule",
filter_from="amazon@amazon.de",
account=account,
action=MailRule.ACTION_MOVE,
action=MailRule.AttachmentAction.MOVE,
action_parameter="spam",
assign_correspondent_from=MailRule.CORRESPONDENT_FROM_EMAIL,
assign_correspondent_from=MailRule.CorrespondentSource.FROM_EMAIL,
)
self.mail_account_handler.handle_mail_account(account)
@@ -684,7 +702,7 @@ class TestMail(DirectoriesMixin, TestCase):
rule = MailRule.objects.create(
name="testrule3",
account=account,
action=MailRule.ACTION_DELETE,
action=MailRule.AttachmentAction.DELETE,
filter_subject="Claim",
)

View File

@@ -8,6 +8,8 @@ from documents.parsers import make_thumbnail_from_pdf
from documents.parsers import ParseError
from PIL import Image
Image.MAX_IMAGE_PIXELS = settings.OCR_MAX_IMAGE_PIXELS
class NoTextFoundException(Exception):
pass

View File

@@ -6,6 +6,8 @@ from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
Image.MAX_IMAGE_PIXELS = settings.OCR_MAX_IMAGE_PIXELS
class TextDocumentParser(DocumentParser):
"""

View File

@@ -7,7 +7,7 @@ max-line-length = 88
[tool:pytest]
DJANGO_SETTINGS_MODULE=paperless.settings
addopts = --pythonwarnings=all --cov --cov-report=html -n auto
addopts = --pythonwarnings=all --cov --cov-report=html --numprocesses auto --quiet
env =
PAPERLESS_DISABLE_DBHANDLER=true