Merge branch 'dev' into feature/any-all-filtering

This commit is contained in:
Michael Shamoon
2022-02-14 22:23:31 -08:00
323 changed files with 85068 additions and 12189 deletions

78
src/documents/admin.py Executable file → Normal file
View File

@@ -1,10 +1,6 @@
from django.contrib import admin
from django.utils.html import format_html, format_html_join
from django.utils.safestring import mark_safe
from whoosh.writing import AsyncWriter
from . import index
from .models import Correspondent, Document, DocumentType, Log, Tag, \
from .models import Correspondent, Document, DocumentType, Tag, \
SavedView, SavedViewFilterRule
@@ -23,12 +19,12 @@ class TagAdmin(admin.ModelAdmin):
list_display = (
"name",
"colour",
"color",
"match",
"matching_algorithm"
)
list_filter = ("colour", "matching_algorithm")
list_editable = ("colour", "match", "matching_algorithm")
list_filter = ("color", "matching_algorithm")
list_editable = ("color", "match", "matching_algorithm")
class DocumentTypeAdmin(admin.ModelAdmin):
@@ -50,26 +46,31 @@ class DocumentAdmin(admin.ModelAdmin):
"modified",
"mime_type",
"storage_type",
"filename")
"filename",
"checksum",
"archive_filename",
"archive_checksum"
)
list_display_links = ("title",)
list_display = (
"correspondent",
"id",
"title",
"tags_",
"created",
"mime_type",
"filename",
"archive_filename"
)
list_filter = (
"document_type",
"tags",
"correspondent"
("mime_type"),
("archive_serial_number", admin.EmptyFieldListFilter),
("archive_filename", admin.EmptyFieldListFilter),
)
filter_horizontal = ("tags",)
ordering = ["-created"]
ordering = ["-id"]
date_hierarchy = "created"
@@ -81,56 +82,24 @@ class DocumentAdmin(admin.ModelAdmin):
created_.short_description = "Created"
def delete_queryset(self, request, queryset):
ix = index.open_index()
with AsyncWriter(ix) as writer:
from documents import index
with index.open_index_writer() as writer:
for o in queryset:
index.remove_document(writer, o)
super(DocumentAdmin, self).delete_queryset(request, queryset)
def delete_model(self, request, obj):
from documents import index
index.remove_document_from_index(obj)
super(DocumentAdmin, self).delete_model(request, obj)
def save_model(self, request, obj, form, change):
from documents import index
index.add_or_update_document(obj)
super(DocumentAdmin, self).save_model(request, obj, form, change)
@mark_safe
def tags_(self, obj):
r = ""
for tag in obj.tags.all():
r += self._html_tag(
"span",
tag.name + ", "
)
return r
@staticmethod
def _html_tag(kind, inside=None, **kwargs):
attributes = format_html_join(' ', '{}="{}"', kwargs.items())
if inside is not None:
return format_html("<{kind} {attributes}>{inside}</{kind}>",
kind=kind, attributes=attributes, inside=inside)
return format_html("<{} {}/>", kind, attributes)
class LogAdmin(admin.ModelAdmin):
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
list_display = ("created", "message", "level",)
list_filter = ("level", "created",)
ordering = ('-created',)
list_display_links = ("created", "message")
class RuleInline(admin.TabularInline):
model = SavedViewFilterRule
@@ -149,5 +118,4 @@ admin.site.register(Correspondent, CorrespondentAdmin)
admin.site.register(Tag, TagAdmin)
admin.site.register(DocumentType, DocumentTypeAdmin)
admin.site.register(Document, DocumentAdmin)
admin.site.register(Log, LogAdmin)
admin.site.register(SavedView, SavedViewAdmin)

View File

@@ -0,0 +1,60 @@
from zipfile import ZipFile
from documents.models import Document
class BulkArchiveStrategy:
def __init__(self, zipf: ZipFile):
self.zipf = zipf
def make_unique_filename(self,
doc: Document,
archive: bool = False,
folder: str = ""):
counter = 0
while True:
filename = folder + doc.get_public_filename(archive, counter)
if filename in self.zipf.namelist():
counter += 1
else:
return filename
def add_document(self, doc: Document):
raise NotImplementedError() # pragma: no cover
class OriginalsOnlyStrategy(BulkArchiveStrategy):
def add_document(self, doc: Document):
self.zipf.write(doc.source_path, self.make_unique_filename(doc))
class ArchiveOnlyStrategy(BulkArchiveStrategy):
def __init__(self, zipf):
super(ArchiveOnlyStrategy, self).__init__(zipf)
def add_document(self, doc: Document):
if doc.has_archive_version:
self.zipf.write(doc.archive_path,
self.make_unique_filename(doc, archive=True))
else:
self.zipf.write(doc.source_path,
self.make_unique_filename(doc))
class OriginalAndArchiveStrategy(BulkArchiveStrategy):
def add_document(self, doc: Document):
if doc.has_archive_version:
self.zipf.write(
doc.archive_path, self.make_unique_filename(
doc, archive=True, folder="archive/"
)
)
self.zipf.write(
doc.source_path,
self.make_unique_filename(doc, folder="originals/")
)

View File

@@ -2,9 +2,7 @@ import itertools
from django.db.models import Q
from django_q.tasks import async_task
from whoosh.writing import AsyncWriter
from documents import index
from documents.models import Document, Correspondent, DocumentType
@@ -99,8 +97,9 @@ def modify_tags(doc_ids, add_tags, remove_tags):
def delete(doc_ids):
Document.objects.filter(id__in=doc_ids).delete()
ix = index.open_index()
with AsyncWriter(ix) as writer:
from documents import index
with index.open_index_writer() as writer:
for id in doc_ids:
index.remove_document_by_id(writer, id)

117
src/documents/classifier.py Executable file → Normal file
View File

@@ -3,12 +3,9 @@ import logging
import os
import pickle
import re
import shutil
from django.conf import settings
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.neural_network import MLPClassifier
from sklearn.preprocessing import MultiLabelBinarizer, LabelBinarizer
from sklearn.utils.multiclass import type_of_target
from documents.models import Document, MatchingModel
@@ -17,7 +14,11 @@ class IncompatibleClassifierVersionError(Exception):
pass
logger = logging.getLogger(__name__)
class ClassifierModelCorruptError(Exception):
pass
logger = logging.getLogger("paperless.classifier")
def preprocess_content(content):
@@ -26,15 +27,46 @@ def preprocess_content(content):
return content
def load_classifier():
if not os.path.isfile(settings.MODEL_FILE):
logger.debug(
f"Document classification model does not exist (yet), not "
f"performing automatic matching."
)
return None
classifier = DocumentClassifier()
try:
classifier.load()
except (ClassifierModelCorruptError,
IncompatibleClassifierVersionError):
# there's something wrong with the model file.
logger.exception(
f"Unrecoverable error while loading document "
f"classification model, deleting model file."
)
os.unlink(settings.MODEL_FILE)
classifier = None
except OSError:
logger.exception(
f"IO error while loading document classification model"
)
classifier = None
except Exception:
logger.exception(
f"Unknown error while loading document classification model"
)
classifier = None
return classifier
class DocumentClassifier(object):
FORMAT_VERSION = 6
def __init__(self):
# mtime of the model file on disk. used to prevent reloading when
# nothing has changed.
self.classifier_version = 0
# hash of the training data. used to prevent re-training when the
# training data has not changed.
self.data_hash = None
@@ -45,20 +77,15 @@ class DocumentClassifier(object):
self.correspondent_classifier = None
self.document_type_classifier = None
def reload(self):
if os.path.getmtime(settings.MODEL_FILE) > self.classifier_version:
with open(settings.MODEL_FILE, "rb") as f:
schema_version = pickle.load(f)
def load(self):
with open(settings.MODEL_FILE, "rb") as f:
schema_version = pickle.load(f)
if schema_version != self.FORMAT_VERSION:
raise IncompatibleClassifierVersionError(
"Cannor load classifier, incompatible versions.")
else:
if self.classifier_version > 0:
# Don't be confused by this check. It's simply here
# so that we wont log anything on initial reload.
logger.info("Classifier updated on disk, "
"reloading classifier models")
if schema_version != self.FORMAT_VERSION:
raise IncompatibleClassifierVersionError(
"Cannor load classifier, incompatible versions.")
else:
try:
self.data_hash = pickle.load(f)
self.data_vectorizer = pickle.load(f)
self.tags_binarizer = pickle.load(f)
@@ -66,10 +93,14 @@ class DocumentClassifier(object):
self.tags_classifier = pickle.load(f)
self.correspondent_classifier = pickle.load(f)
self.document_type_classifier = pickle.load(f)
self.classifier_version = os.path.getmtime(settings.MODEL_FILE)
except Exception:
raise ClassifierModelCorruptError()
def save_classifier(self):
with open(settings.MODEL_FILE, "wb") as f:
def save(self):
target_file = settings.MODEL_FILE
target_file_temp = settings.MODEL_FILE + ".part"
with open(target_file_temp, "wb") as f:
pickle.dump(self.FORMAT_VERSION, f)
pickle.dump(self.data_hash, f)
pickle.dump(self.data_vectorizer, f)
@@ -80,14 +111,19 @@ class DocumentClassifier(object):
pickle.dump(self.correspondent_classifier, f)
pickle.dump(self.document_type_classifier, f)
if os.path.isfile(target_file):
os.unlink(target_file)
shutil.move(target_file_temp, target_file)
def train(self):
data = list()
labels_tags = list()
labels_correspondent = list()
labels_document_type = list()
# Step 1: Extract and preprocess training data from the database.
logging.getLogger(__name__).debug("Gathering data from database...")
logger.debug("Gathering data from database...")
m = hashlib.sha1()
for doc in Document.objects.order_by('pk').exclude(tags__is_inbox_tag=True): # NOQA: E501
preprocessed_content = preprocess_content(doc.content)
@@ -108,10 +144,11 @@ class DocumentClassifier(object):
m.update(y.to_bytes(4, 'little', signed=True))
labels_correspondent.append(y)
tags = [tag.pk for tag in doc.tags.filter(
tags = sorted([tag.pk for tag in doc.tags.filter(
matching_algorithm=MatchingModel.MATCH_AUTO
)]
m.update(bytearray(tags))
)])
for tag in tags:
m.update(tag.to_bytes(4, 'little', signed=True))
labels_tags.append(tags)
if not data:
@@ -134,7 +171,7 @@ class DocumentClassifier(object):
num_correspondents = len(set(labels_correspondent) | {-1}) - 1
num_document_types = len(set(labels_document_type) | {-1}) - 1
logging.getLogger(__name__).debug(
logger.debug(
"{} documents, {} tag(s), {} correspondent(s), "
"{} document type(s).".format(
len(data),
@@ -144,8 +181,12 @@ class DocumentClassifier(object):
)
)
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.neural_network import MLPClassifier
from sklearn.preprocessing import MultiLabelBinarizer, LabelBinarizer
# Step 2: vectorize data
logging.getLogger(__name__).debug("Vectorizing data...")
logger.debug("Vectorizing data...")
self.data_vectorizer = CountVectorizer(
analyzer="word",
ngram_range=(1, 2),
@@ -155,7 +196,7 @@ class DocumentClassifier(object):
# Step 3: train the classifiers
if num_tags > 0:
logging.getLogger(__name__).debug("Training tags classifier...")
logger.debug("Training tags classifier...")
if num_tags == 1:
# Special case where only one tag has auto:
@@ -174,12 +215,12 @@ class DocumentClassifier(object):
self.tags_classifier.fit(data_vectorized, labels_tags_vectorized)
else:
self.tags_classifier = None
logging.getLogger(__name__).debug(
logger.debug(
"There are no tags. Not training tags classifier."
)
if num_correspondents > 0:
logging.getLogger(__name__).debug(
logger.debug(
"Training correspondent classifier..."
)
self.correspondent_classifier = MLPClassifier(tol=0.01)
@@ -189,13 +230,13 @@ class DocumentClassifier(object):
)
else:
self.correspondent_classifier = None
logging.getLogger(__name__).debug(
logger.debug(
"There are no correspondents. Not training correspondent "
"classifier."
)
if num_document_types > 0:
logging.getLogger(__name__).debug(
logger.debug(
"Training document type classifier..."
)
self.document_type_classifier = MLPClassifier(tol=0.01)
@@ -205,7 +246,7 @@ class DocumentClassifier(object):
)
else:
self.document_type_classifier = None
logging.getLogger(__name__).debug(
logger.debug(
"There are no document types. Not training document type "
"classifier."
)
@@ -237,6 +278,8 @@ class DocumentClassifier(object):
return None
def predict_tags(self, content):
from sklearn.utils.multiclass import type_of_target
if self.tags_classifier:
X = self.data_vectorizer.transform([preprocess_content(content)])
y = self.tags_classifier.predict(X)

172
src/documents/consumer.py Executable file → Normal file
View File

@@ -1,9 +1,12 @@
import datetime
import hashlib
import os
import uuid
from subprocess import Popen
import magic
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.conf import settings
from django.db import transaction
from django.db.models import Q
@@ -11,7 +14,7 @@ from django.utils import timezone
from filelock import FileLock
from rest_framework.reverse import reverse
from .classifier import DocumentClassifier, IncompatibleClassifierVersionError
from .classifier import load_classifier
from .file_handling import create_source_path_directory, \
generate_unique_filename
from .loggers import LoggingMixin
@@ -27,8 +30,45 @@ class ConsumerError(Exception):
pass
MESSAGE_DOCUMENT_ALREADY_EXISTS = "document_already_exists"
MESSAGE_FILE_NOT_FOUND = "file_not_found"
MESSAGE_PRE_CONSUME_SCRIPT_NOT_FOUND = "pre_consume_script_not_found"
MESSAGE_PRE_CONSUME_SCRIPT_ERROR = "pre_consume_script_error"
MESSAGE_POST_CONSUME_SCRIPT_NOT_FOUND = "post_consume_script_not_found"
MESSAGE_POST_CONSUME_SCRIPT_ERROR = "post_consume_script_error"
MESSAGE_NEW_FILE = "new_file"
MESSAGE_UNSUPPORTED_TYPE = "unsupported_type"
MESSAGE_PARSING_DOCUMENT = "parsing_document"
MESSAGE_GENERATING_THUMBNAIL = "generating_thumbnail"
MESSAGE_PARSE_DATE = "parse_date"
MESSAGE_SAVE_DOCUMENT = "save_document"
MESSAGE_FINISHED = "finished"
class Consumer(LoggingMixin):
logging_name = "paperless.consumer"
def _send_progress(self, current_progress, max_progress, status,
message=None, document_id=None):
payload = {
'filename': os.path.basename(self.filename) if self.filename else None, # NOQA: E501
'task_id': self.task_id,
'current_progress': current_progress,
'max_progress': max_progress,
'status': status,
'message': message,
'document_id': document_id
}
async_to_sync(self.channel_layer.group_send)("status_updates",
{'type': 'status_update',
'data': payload})
def _fail(self, message, log_message=None, exc_info=None):
self._send_progress(100, 100, 'FAILED', message)
self.log("error", log_message or message, exc_info=exc_info)
raise ConsumerError(f"{self.filename}: {log_message or message}")
def __init__(self):
super().__init__()
self.path = None
@@ -37,15 +77,16 @@ class Consumer(LoggingMixin):
self.override_correspondent_id = None
self.override_tag_ids = None
self.override_document_type_id = None
self.task_id = None
self.channel_layer = get_channel_layer()
def pre_check_file_exists(self):
if not os.path.isfile(self.path):
self.log(
"error",
"Cannot consume {}: It is not a file.".format(self.path)
self._fail(
MESSAGE_FILE_NOT_FOUND,
f"Cannot consume {self.path}: File not found."
)
raise ConsumerError("Cannot consume {}: It is not a file".format(
self.path))
def pre_check_duplicate(self):
with open(self.path, "rb") as f:
@@ -53,12 +94,9 @@ class Consumer(LoggingMixin):
if Document.objects.filter(Q(checksum=checksum) | Q(archive_checksum=checksum)).exists(): # NOQA: E501
if settings.CONSUMER_DELETE_DUPLICATES:
os.unlink(self.path)
self.log(
"error",
"Not consuming {}: It is a duplicate.".format(self.filename)
)
raise ConsumerError(
"Not consuming {}: It is a duplicate.".format(self.filename)
self._fail(
MESSAGE_DOCUMENT_ALREADY_EXISTS,
f"Not consuming {self.filename}: It is a duplicate."
)
def pre_check_directories(self):
@@ -72,15 +110,21 @@ class Consumer(LoggingMixin):
return
if not os.path.isfile(settings.PRE_CONSUME_SCRIPT):
raise ConsumerError(
self._fail(
MESSAGE_PRE_CONSUME_SCRIPT_NOT_FOUND,
f"Configured pre-consume script "
f"{settings.PRE_CONSUME_SCRIPT} does not exist.")
self.log("info",
f"Executing pre-consume script {settings.PRE_CONSUME_SCRIPT}")
try:
Popen((settings.PRE_CONSUME_SCRIPT, self.path)).wait()
except Exception as e:
raise ConsumerError(
f"Error while executing pre-consume script: {e}"
self._fail(
MESSAGE_PRE_CONSUME_SCRIPT_ERROR,
f"Error while executing pre-consume script: {e}",
exc_info=True
)
def run_post_consume_script(self, document):
@@ -88,9 +132,16 @@ class Consumer(LoggingMixin):
return
if not os.path.isfile(settings.POST_CONSUME_SCRIPT):
raise ConsumerError(
self._fail(
MESSAGE_POST_CONSUME_SCRIPT_NOT_FOUND,
f"Configured post-consume script "
f"{settings.POST_CONSUME_SCRIPT} does not exist.")
f"{settings.POST_CONSUME_SCRIPT} does not exist."
)
self.log(
"info",
f"Executing post-consume script {settings.POST_CONSUME_SCRIPT}"
)
try:
Popen((
@@ -106,8 +157,10 @@ class Consumer(LoggingMixin):
"name", flat=True)))
)).wait()
except Exception as e:
raise ConsumerError(
f"Error while executing pre-consume script: {e}"
self._fail(
MESSAGE_POST_CONSUME_SCRIPT_ERROR,
f"Error while executing post-consume script: {e}",
exc_info=True
)
def try_consume_file(self,
@@ -116,7 +169,8 @@ class Consumer(LoggingMixin):
override_title=None,
override_correspondent_id=None,
override_document_type_id=None,
override_tag_ids=None):
override_tag_ids=None,
task_id=None):
"""
Return the document object if it was successfully created.
"""
@@ -127,6 +181,9 @@ class Consumer(LoggingMixin):
self.override_correspondent_id = override_correspondent_id
self.override_document_type_id = override_document_type_id
self.override_tag_ids = override_tag_ids
self.task_id = task_id or str(uuid.uuid4())
self._send_progress(0, 100, 'STARTING', MESSAGE_NEW_FILE)
# this is for grouping logging entries for this particular file
# together.
@@ -149,11 +206,10 @@ class Consumer(LoggingMixin):
parser_class = get_parser_class_for_mime_type(mime_type)
if not parser_class:
raise ConsumerError(
f"Unsupported mime type {mime_type} of file {self.filename}")
else:
self.log("debug",
f"Parser: {parser_class.__name__}")
self._fail(
MESSAGE_UNSUPPORTED_TYPE,
f"Unsupported mime type {mime_type}"
)
# Notify all listeners that we're going to do some work.
@@ -165,35 +221,53 @@ class Consumer(LoggingMixin):
self.run_pre_consume_script()
def progress_callback(current_progress, max_progress):
# recalculate progress to be within 20 and 80
p = int((current_progress / max_progress) * 50 + 20)
self._send_progress(p, 100, "WORKING")
# This doesn't parse the document yet, but gives us a parser.
document_parser = parser_class(self.logging_group)
document_parser = parser_class(self.logging_group, progress_callback)
self.log("debug", f"Parser: {type(document_parser).__name__}")
# However, this already created working directories which we have to
# clean up.
# Parse the document. This may take some time.
text = None
date = None
thumbnail = None
archive_path = None
try:
self._send_progress(20, 100, 'WORKING', MESSAGE_PARSING_DOCUMENT)
self.log("debug", "Parsing {}...".format(self.filename))
document_parser.parse(self.path, mime_type, self.filename)
self.log("debug", f"Generating thumbnail for {self.filename}...")
self._send_progress(70, 100, 'WORKING',
MESSAGE_GENERATING_THUMBNAIL)
thumbnail = document_parser.get_optimised_thumbnail(
self.path, mime_type)
self.path, mime_type, self.filename)
text = document_parser.get_text()
date = document_parser.get_date()
if not date:
self._send_progress(90, 100, 'WORKING',
MESSAGE_PARSE_DATE)
date = parse_date(self.filename, text)
archive_path = document_parser.get_archive_path()
except ParseError as e:
document_parser.cleanup()
self.log(
"error",
f"Error while consuming document {self.filename}: {e}")
raise ConsumerError(e)
self._fail(
str(e),
f"Error while consuming document {self.filename}: {e}",
exc_info=True
)
# Prepare the document classifier.
@@ -201,15 +275,9 @@ class Consumer(LoggingMixin):
# reloading the classifier multiple times, since there are multiple
# post-consume hooks that all require the classifier.
try:
classifier = DocumentClassifier()
classifier.reload()
except (OSError, EOFError, IncompatibleClassifierVersionError) as e:
self.log(
"warning",
f"Cannot classify documents: {e}.")
classifier = None
classifier = load_classifier()
self._send_progress(95, 100, 'WORKING', MESSAGE_SAVE_DOCUMENT)
# now that everything is done, we can start to store the document
# in the system. This will be a transaction and reasonably fast.
try:
@@ -235,8 +303,7 @@ class Consumer(LoggingMixin):
# After everything is in the database, copy the files into
# place. If this fails, we'll also rollback the transaction.
with FileLock(settings.MEDIA_LOCK):
document.filename = generate_unique_filename(
document, settings.ORIGINALS_DIR)
document.filename = generate_unique_filename(document)
create_source_path_directory(document.source_path)
self._write(document.storage_type,
@@ -246,6 +313,10 @@ class Consumer(LoggingMixin):
thumbnail, document.thumbnail_path)
if archive_path and os.path.isfile(archive_path):
document.archive_filename = generate_unique_filename(
document,
archive_filename=True
)
create_source_path_directory(document.archive_path)
self._write(document.storage_type,
archive_path, document.archive_path)
@@ -262,13 +333,22 @@ class Consumer(LoggingMixin):
self.log("debug", "Deleting file {}".format(self.path))
os.unlink(self.path)
# https://github.com/jonaswinkler/paperless-ng/discussions/1037
shadow_file = os.path.join(
os.path.dirname(self.path),
"._" + os.path.basename(self.path))
if os.path.isfile(shadow_file):
self.log("debug", "Deleting file {}".format(shadow_file))
os.unlink(shadow_file)
except Exception as e:
self.log(
"error",
self._fail(
str(e),
f"The following error occured while consuming "
f"{self.filename}: {e}"
f"{self.filename}: {e}",
exc_info=True
)
raise ConsumerError(e)
finally:
document_parser.cleanup()
@@ -279,6 +359,8 @@ class Consumer(LoggingMixin):
"Document {} consumption finished".format(document)
)
self._send_progress(100, 100, 'SUCCESS', MESSAGE_FINISHED, document.id)
return document
def _store(self, text, date, mime_type):

View File

@@ -8,6 +8,9 @@ from django.conf import settings
from django.template.defaultfilters import slugify
logger = logging.getLogger("paperless.filehandling")
class defaultdictNoStr(defaultdict):
def __str__(self):
@@ -76,12 +79,40 @@ def many_to_dictionary(field):
return mydictionary
def generate_unique_filename(doc, root):
def generate_unique_filename(doc,
archive_filename=False):
"""
Generates a unique filename for doc in settings.ORIGINALS_DIR.
The returned filename is guaranteed to be either the current filename
of the document if unchanged, or a new filename that does not correspondent
to any existing files. The function will append _01, _02, etc to the
filename before the extension to avoid conflicts.
If archive_filename is True, return a unique archive filename instead.
"""
if archive_filename:
old_filename = doc.archive_filename
root = settings.ARCHIVE_DIR
else:
old_filename = doc.filename
root = settings.ORIGINALS_DIR
# If generating archive filenames, try to make a name that is similar to
# the original filename first.
if archive_filename and doc.filename:
new_filename = os.path.splitext(doc.filename)[0] + ".pdf"
if new_filename == old_filename or not os.path.exists(os.path.join(root, new_filename)): # NOQA: E501
return new_filename
counter = 0
while True:
new_filename = generate_filename(doc, counter)
if new_filename == doc.filename:
new_filename = generate_filename(
doc, counter, archive_filename=archive_filename)
if new_filename == old_filename:
# still the same as before.
return new_filename
@@ -91,7 +122,7 @@ def generate_unique_filename(doc, root):
return new_filename
def generate_filename(doc, counter=0, append_gpg=True):
def generate_filename(doc, counter=0, append_gpg=True, archive_filename=False):
path = ""
try:
@@ -120,6 +151,11 @@ def generate_filename(doc, counter=0, append_gpg=True):
else:
document_type = "none"
if doc.archive_serial_number:
asn = str(doc.archive_serial_number)
else:
asn = "none"
path = settings.PAPERLESS_FILENAME_FORMAT.format(
title=pathvalidate.sanitize_filename(
doc.title, replacement_text="-"),
@@ -133,6 +169,7 @@ def generate_filename(doc, counter=0, append_gpg=True):
added_year=doc.added.year if doc.added else "none",
added_month=f"{doc.added.month:02}" if doc.added else "none",
added_day=f"{doc.added.day:02}" if doc.added else "none",
asn=asn,
tags=tags,
tag_list=tag_list
).strip()
@@ -140,23 +177,21 @@ def generate_filename(doc, counter=0, append_gpg=True):
path = path.strip(os.sep)
except (ValueError, KeyError, IndexError):
logging.getLogger(__name__).warning(
logger.warning(
f"Invalid PAPERLESS_FILENAME_FORMAT: "
f"{settings.PAPERLESS_FILENAME_FORMAT}, falling back to default")
counter_str = f"_{counter:02}" if counter else ""
filetype_str = ".pdf" if archive_filename else doc.file_type
if len(path) > 0:
filename = f"{path}{counter_str}{doc.file_type}"
filename = f"{path}{counter_str}{filetype_str}"
else:
filename = f"{doc.pk:07}{counter_str}{doc.file_type}"
filename = f"{doc.pk:07}{counter_str}{filetype_str}"
# Append .gpg for encrypted files
if append_gpg and doc.storage_type == doc.STORAGE_TYPE_GPG:
filename += ".gpg"
return filename
def archive_name_from_filename(filename):
return os.path.splitext(filename)[0] + ".pdf"

13
src/documents/filters.py Executable file → Normal file
View File

@@ -1,3 +1,4 @@
from django.db.models import Q
from django_filters.rest_framework import BooleanFilter, FilterSet, Filter
from .models import Correspondent, Document, Tag, DocumentType, Log
@@ -74,6 +75,16 @@ class InboxFilter(Filter):
return qs
class TitleContentFilter(Filter):
def filter(self, qs, value):
if value:
return qs.filter(Q(title__icontains=value) |
Q(content__icontains=value))
else:
return qs
class DocumentFilterSet(FilterSet):
is_tagged = BooleanFilter(
@@ -91,6 +102,8 @@ class DocumentFilterSet(FilterSet):
is_in_inbox = InboxFilter()
title_content = TitleContentFilter()
class Meta:
model = Document
fields = {

View File

@@ -2,75 +2,70 @@ import logging
import os
from contextlib import contextmanager
import math
from dateutil.parser import isoparse
from django.conf import settings
from whoosh import highlight, classify, query
from whoosh.fields import Schema, TEXT, NUMERIC, KEYWORD, DATETIME
from whoosh.highlight import Formatter, get_text
from whoosh.fields import Schema, TEXT, NUMERIC, KEYWORD, DATETIME, BOOLEAN
from whoosh.highlight import HtmlFormatter
from whoosh.index import create_in, exists_in, open_dir
from whoosh.qparser import MultifieldParser
from whoosh.qparser.dateparse import DateParserPlugin
from whoosh.searching import ResultsPage, Searcher
from whoosh.writing import AsyncWriter
from documents.models import Document
logger = logging.getLogger(__name__)
class JsonFormatter(Formatter):
def __init__(self):
self.seen = {}
def format_token(self, text, token, replace=False):
ttext = self._text(get_text(text, token, replace))
return {'text': ttext, 'highlight': 'true'}
def format_fragment(self, fragment, replace=False):
output = []
index = fragment.startchar
text = fragment.text
amend_token = None
for t in fragment.matches:
if t.startchar is None:
continue
if t.startchar < index:
continue
if t.startchar > index:
text_inbetween = text[index:t.startchar]
if amend_token and t.startchar - index < 10:
amend_token['text'] += text_inbetween
else:
output.append({'text': text_inbetween,
'highlight': False})
amend_token = None
token = self.format_token(text, t, replace)
if amend_token:
amend_token['text'] += token['text']
else:
output.append(token)
amend_token = token
index = t.endchar
if index < fragment.endchar:
output.append({'text': text[index:fragment.endchar],
'highlight': False})
return output
def format(self, fragments, replace=False):
output = []
for fragment in fragments:
output.append(self.format_fragment(fragment, replace=replace))
return output
logger = logging.getLogger("paperless.index")
def get_schema():
return Schema(
id=NUMERIC(stored=True, unique=True, numtype=int),
title=TEXT(stored=True),
id=NUMERIC(
stored=True,
unique=True
),
title=TEXT(
sortable=True
),
content=TEXT(),
correspondent=TEXT(stored=True),
tag=KEYWORD(stored=True, commas=True, scorable=True, lowercase=True),
type=TEXT(stored=True),
created=DATETIME(stored=True, sortable=True),
modified=DATETIME(stored=True, sortable=True),
added=DATETIME(stored=True, sortable=True),
asn=NUMERIC(
sortable=True
),
correspondent=TEXT(
sortable=True
),
correspondent_id=NUMERIC(),
has_correspondent=BOOLEAN(),
tag=KEYWORD(
commas=True,
scorable=True,
lowercase=True
),
tag_id=KEYWORD(
commas=True,
scorable=True
),
has_tag=BOOLEAN(),
type=TEXT(
sortable=True
),
type_id=NUMERIC(),
has_type=BOOLEAN(),
created=DATETIME(
sortable=True
),
modified=DATETIME(
sortable=True
),
added=DATETIME(
sortable=True
),
)
@@ -78,25 +73,56 @@ def open_index(recreate=False):
try:
if exists_in(settings.INDEX_DIR) and not recreate:
return open_dir(settings.INDEX_DIR, schema=get_schema())
except Exception as e:
logger.error(f"Error while opening the index: {e}, recreating.")
except Exception:
logger.exception(f"Error while opening the index, recreating.")
if not os.path.isdir(settings.INDEX_DIR):
os.makedirs(settings.INDEX_DIR, exist_ok=True)
return create_in(settings.INDEX_DIR, get_schema())
@contextmanager
def open_index_writer(optimize=False):
writer = AsyncWriter(open_index())
try:
yield writer
except Exception as e:
logger.exception(str(e))
writer.cancel()
finally:
writer.commit(optimize=optimize)
@contextmanager
def open_index_searcher():
searcher = open_index().searcher()
try:
yield searcher
finally:
searcher.close()
def update_document(writer, doc):
tags = ",".join([t.name for t in doc.tags.all()])
tags_ids = ",".join([str(t.id) for t in doc.tags.all()])
writer.update_document(
id=doc.pk,
title=doc.title,
content=doc.content,
correspondent=doc.correspondent.name if doc.correspondent else None,
correspondent_id=doc.correspondent.id if doc.correspondent else None,
has_correspondent=doc.correspondent is not None,
tag=tags if tags else None,
tag_id=tags_ids if tags_ids else None,
has_tag=len(tags) > 0,
type=doc.document_type.name if doc.document_type else None,
type_id=doc.document_type.id if doc.document_type else None,
has_type=doc.document_type is not None,
created=doc.created,
added=doc.added,
asn=doc.archive_serial_number,
modified=doc.modified,
)
@@ -110,61 +136,162 @@ def remove_document_by_id(writer, doc_id):
def add_or_update_document(document):
ix = open_index()
with AsyncWriter(ix) as writer:
with open_index_writer() as writer:
update_document(writer, document)
def remove_document_from_index(document):
ix = open_index()
with AsyncWriter(ix) as writer:
with open_index_writer() as writer:
remove_document(writer, document)
@contextmanager
def query_page(ix, page, querystring, more_like_doc_id, more_like_doc_content):
searcher = ix.searcher()
try:
if querystring:
qp = MultifieldParser(
["content", "title", "correspondent", "tag", "type"],
ix.schema)
qp.add_plugin(DateParserPlugin())
str_q = qp.parse(querystring)
corrected = searcher.correct_query(str_q, querystring)
else:
str_q = None
corrected = None
class DelayedQuery:
if more_like_doc_id:
docnum = searcher.document_number(id=more_like_doc_id)
kts = searcher.key_terms_from_text(
'content', more_like_doc_content, numterms=20,
model=classify.Bo1Model, normalize=False)
more_like_q = query.Or(
[query.Term('content', word, boost=weight)
for word, weight in kts])
result_page = searcher.search_page(
more_like_q, page, filter=str_q, mask={docnum})
elif str_q:
result_page = searcher.search_page(str_q, page)
else:
raise ValueError(
"Either querystring or more_like_doc_id is required."
)
def _get_query(self):
raise NotImplementedError()
result_page.results.fragmenter = highlight.ContextFragmenter(
def _get_query_filter(self):
criterias = []
for k, v in self.query_params.items():
if k == 'correspondent__id':
criterias.append(query.Term('correspondent_id', v))
elif k == 'tags__id__all':
for tag_id in v.split(","):
criterias.append(query.Term('tag_id', tag_id))
elif k == 'document_type__id':
criterias.append(query.Term('type_id', v))
elif k == 'correspondent__isnull':
criterias.append(query.Term("has_correspondent", v == "false"))
elif k == 'is_tagged':
criterias.append(query.Term("has_tag", v == "true"))
elif k == 'document_type__isnull':
criterias.append(query.Term("has_type", v == "false"))
elif k == 'created__date__lt':
criterias.append(
query.DateRange("created", start=None, end=isoparse(v)))
elif k == 'created__date__gt':
criterias.append(
query.DateRange("created", start=isoparse(v), end=None))
elif k == 'added__date__gt':
criterias.append(
query.DateRange("added", start=isoparse(v), end=None))
elif k == 'added__date__lt':
criterias.append(
query.DateRange("added", start=None, end=isoparse(v)))
if len(criterias) > 0:
return query.And(criterias)
else:
return None
def _get_query_sortedby(self):
if 'ordering' not in self.query_params:
return None, False
field: str = self.query_params['ordering']
sort_fields_map = {
"created": "created",
"modified": "modified",
"added": "added",
"title": "title",
"correspondent__name": "correspondent",
"document_type__name": "type",
"archive_serial_number": "asn"
}
if field.startswith('-'):
field = field[1:]
reverse = True
else:
reverse = False
if field not in sort_fields_map:
return None, False
else:
return sort_fields_map[field], reverse
def __init__(self, searcher: Searcher, query_params, page_size):
self.searcher = searcher
self.query_params = query_params
self.page_size = page_size
self.saved_results = dict()
self.first_score = None
def __len__(self):
page = self[0:1]
return len(page)
def __getitem__(self, item):
if item.start in self.saved_results:
return self.saved_results[item.start]
q, mask = self._get_query()
sortedby, reverse = self._get_query_sortedby()
page: ResultsPage = self.searcher.search_page(
q,
mask=mask,
filter=self._get_query_filter(),
pagenum=math.floor(item.start / self.page_size) + 1,
pagelen=self.page_size,
sortedby=sortedby,
reverse=reverse
)
page.results.fragmenter = highlight.ContextFragmenter(
surround=50)
result_page.results.formatter = JsonFormatter()
page.results.formatter = HtmlFormatter(tagname="span", between=" ... ")
if corrected and corrected.query != str_q:
if (not self.first_score and
len(page.results) > 0 and
sortedby is None):
self.first_score = page.results[0].score
page.results.top_n = list(map(
lambda hit: (
(hit[0] / self.first_score) if self.first_score else None,
hit[1]
),
page.results.top_n
))
self.saved_results[item.start] = page
return page
class DelayedFullTextQuery(DelayedQuery):
def _get_query(self):
q_str = self.query_params['query']
qp = MultifieldParser(
["content", "title", "correspondent", "tag", "type"],
self.searcher.ixreader.schema)
qp.add_plugin(DateParserPlugin())
q = qp.parse(q_str)
corrected = self.searcher.correct_query(q, q_str)
if corrected.query != q:
corrected_query = corrected.string
else:
corrected_query = None
yield result_page, corrected_query
finally:
searcher.close()
return q, None
class DelayedMoreLikeThisQuery(DelayedQuery):
def _get_query(self):
more_like_doc_id = int(self.query_params['more_like_id'])
content = Document.objects.get(id=more_like_doc_id).content
docnum = self.searcher.document_number(id=more_like_doc_id)
kts = self.searcher.key_terms_from_text(
'content', content, numterms=20,
model=classify.Bo1Model, normalize=False)
q = query.Or(
[query.Term('content', word, boost=weight)
for word, weight in kts])
mask = {docnum}
return q, mask
def autocomplete(ix, term, limit=10):

View File

@@ -4,33 +4,24 @@ import uuid
from django.conf import settings
class PaperlessHandler(logging.Handler):
def emit(self, record):
if settings.DISABLE_DBHANDLER:
return
# We have to do the import here or Django will barf when it tries to
# load this because the apps aren't loaded at that point
from .models import Log
kwargs = {"message": record.msg, "level": record.levelno}
if hasattr(record, "group"):
kwargs["group"] = record.group
Log.objects.create(**kwargs)
class LoggingMixin:
logging_group = None
logging_name = None
def renew_logging_group(self):
self.logging_group = uuid.uuid4()
def log(self, level, message, **kwargs):
target = ".".join([self.__class__.__module__, self.__class__.__name__])
logger = logging.getLogger(target)
if self.logging_name:
logger = logging.getLogger(self.logging_name)
else:
name = ".".join([
self.__class__.__module__,
self.__class__.__name__
])
logger = logging.getLogger(name)
getattr(logger, level)(message, extra={
"group": self.logging_group

View File

@@ -16,12 +16,12 @@ from whoosh.writing import AsyncWriter
from documents.models import Document
from ... import index
from ...file_handling import create_source_path_directory
from ...mixins import Renderable
from ...file_handling import create_source_path_directory, \
generate_unique_filename
from ...parsers import get_parser_class_for_mime_type
logger = logging.getLogger(__name__)
logger = logging.getLogger("paperless.management.archiver")
def handle_document(document_id):
@@ -31,38 +31,57 @@ def handle_document(document_id):
parser_class = get_parser_class_for_mime_type(mime_type)
if not parser_class:
logger.error(f"No parser found for mime type {mime_type}, cannot "
f"archive document {document} (ID: {document_id})")
return
parser = parser_class(logging_group=uuid.uuid4())
try:
parser.parse(document.source_path, mime_type)
parser.parse(
document.source_path,
mime_type,
document.get_public_filename())
thumbnail = parser.get_optimised_thumbnail(
document.source_path,
mime_type,
document.get_public_filename()
)
if parser.get_archive_path():
with transaction.atomic():
with open(parser.get_archive_path(), 'rb') as f:
checksum = hashlib.md5(f.read()).hexdigest()
# i'm going to save first so that in case the file move
# I'm going to save first so that in case the file move
# fails, the database is rolled back.
# we also don't use save() since that triggers the filehandling
# We also don't use save() since that triggers the filehandling
# logic, and we don't want that yet (file not yet in place)
document.archive_filename = generate_unique_filename(
document, archive_filename=True)
Document.objects.filter(pk=document.pk).update(
archive_checksum=checksum,
content=parser.get_text()
content=parser.get_text(),
archive_filename=document.archive_filename
)
with FileLock(settings.MEDIA_LOCK):
create_source_path_directory(document.archive_path)
shutil.move(parser.get_archive_path(),
document.archive_path)
shutil.move(thumbnail, document.thumbnail_path)
with AsyncWriter(index.open_index()) as writer:
index.update_document(writer, document)
with index.open_index_writer() as writer:
index.update_document(writer, document)
except Exception as e:
logger.error(f"Error while parsing document {document}: {str(e)}")
logger.exception(f"Error while parsing document {document} "
f"(ID: {document_id})")
finally:
parser.cleanup()
class Command(Renderable, BaseCommand):
class Command(BaseCommand):
help = """
Using the current classification model, assigns correspondents, tags
@@ -71,10 +90,6 @@ class Command(Renderable, BaseCommand):
modified) after their initial import.
""".replace(" ", "")
def __init__(self, *args, **kwargs):
self.verbosity = 0
BaseCommand.__init__(self, *args, **kwargs)
def add_arguments(self, parser):
parser.add_argument(
"-f", "--overwrite",
@@ -91,6 +106,12 @@ class Command(Renderable, BaseCommand):
help="Specify the ID of a document, and this command will only "
"run on this specific document."
)
parser.add_argument(
"--no-progress-bar",
default=False,
action="store_true",
help="If set, the progress bar will not be shown"
)
def handle(self, *args, **options):
@@ -106,7 +127,7 @@ class Command(Renderable, BaseCommand):
document_ids = list(map(
lambda doc: doc.id,
filter(
lambda d: overwrite or not d.archive_checksum,
lambda d: overwrite or not d.has_archive_version,
documents
)
))
@@ -125,7 +146,8 @@ class Command(Renderable, BaseCommand):
handle_document,
document_ids
),
total=len(document_ids)
total=len(document_ids),
disable=options['no_progress_bar']
))
except KeyboardInterrupt:
print("Aborting...")

View File

@@ -1,6 +1,7 @@
import logging
import os
from pathlib import Path
from pathlib import Path, PurePath
from threading import Thread
from time import sleep
from django.conf import settings
@@ -17,11 +18,11 @@ try:
except ImportError:
INotify = flags = None
logger = logging.getLogger(__name__)
logger = logging.getLogger("paperless.management.consumer")
def _tags_from_path(filepath):
"""Walk up the directory tree from filepath to CONSUMPTION_DIr
"""Walk up the directory tree from filepath to CONSUMPTION_DIR
and get or create Tag IDs for every directory.
"""
tag_ids = set()
@@ -35,8 +36,15 @@ def _tags_from_path(filepath):
return tag_ids
def _is_ignored(filepath: str) -> bool:
filepath_relative = PurePath(filepath).relative_to(
settings.CONSUMPTION_DIR)
return any(
filepath_relative.match(p) for p in settings.CONSUMER_IGNORE_PATTERNS)
def _consume(filepath):
if os.path.isdir(filepath):
if os.path.isdir(filepath) or _is_ignored(filepath):
return
if not os.path.isfile(filepath):
@@ -54,10 +62,10 @@ def _consume(filepath):
if settings.CONSUMER_SUBDIRS_AS_TAGS:
tag_ids = _tags_from_path(filepath)
except Exception as e:
logger.error(
"Error creating tags from path: {}".format(e))
logger.exception("Error creating tags from path")
try:
logger.info(f"Adding {filepath} to the task queue.")
async_task("documents.tasks.consume_file",
filepath,
override_tag_ids=tag_ids if tag_ids else None,
@@ -66,14 +74,17 @@ def _consume(filepath):
# Catch all so that the consumer won't crash.
# This is also what the test case is listening for to check for
# errors.
logger.error(
"Error while consuming document: {}".format(e))
logger.exception("Error while consuming document")
def _consume_wait_unmodified(file, num_tries=20, wait_time=1):
def _consume_wait_unmodified(file):
if _is_ignored(file):
return
logger.debug(f"Waiting for file {file} to remain unmodified")
mtime = -1
current_try = 0
while current_try < num_tries:
while current_try < settings.CONSUMER_POLLING_RETRY_COUNT:
try:
new_mtime = os.stat(file).st_mtime
except FileNotFoundError:
@@ -84,7 +95,7 @@ def _consume_wait_unmodified(file, num_tries=20, wait_time=1):
_consume(file)
return
mtime = new_mtime
sleep(wait_time)
sleep(settings.CONSUMER_POLLING_DELAY)
current_try += 1
logger.error(f"Timeout while waiting on file {file} to remain unmodified.")
@@ -93,10 +104,14 @@ def _consume_wait_unmodified(file, num_tries=20, wait_time=1):
class Handler(FileSystemEventHandler):
def on_created(self, event):
_consume_wait_unmodified(event.src_path)
Thread(
target=_consume_wait_unmodified, args=(event.src_path,)
).start()
def on_moved(self, event):
_consume_wait_unmodified(event.dest_path)
Thread(
target=_consume_wait_unmodified, args=(event.dest_path,)
).start()
class Command(BaseCommand):
@@ -108,12 +123,7 @@ class Command(BaseCommand):
# This is here primarily for the tests and is irrelevant in production.
stop_flag = False
def __init__(self, *args, **kwargs):
self.logger = logging.getLogger(__name__)
BaseCommand.__init__(self, *args, **kwargs)
self.observer = None
observer = None
def add_arguments(self, parser):
parser.add_argument(
@@ -153,7 +163,7 @@ class Command(BaseCommand):
if options["oneshot"]:
return
if settings.CONSUMER_POLLING == 0:
if settings.CONSUMER_POLLING == 0 and INotify:
self.handle_inotify(directory, recursive)
else:
self.handle_polling(directory, recursive)
@@ -161,7 +171,7 @@ class Command(BaseCommand):
logger.debug("Consumer exiting.")
def handle_polling(self, directory, recursive):
logging.getLogger(__name__).info(
logger.info(
f"Polling directory for changes: {directory}")
self.observer = PollingObserver(timeout=settings.CONSUMER_POLLING)
self.observer.schedule(Handler(), directory, recursive=recursive)
@@ -176,7 +186,7 @@ class Command(BaseCommand):
self.observer.join()
def handle_inotify(self, directory, recursive):
logging.getLogger(__name__).info(
logger.info(
f"Using inotify to watch directory for changes: {directory}")
inotify = INotify()

View File

@@ -1,10 +1,9 @@
from django.core.management.base import BaseCommand
from ...mixins import Renderable
from ...tasks import train_classifier
class Command(Renderable, BaseCommand):
class Command(BaseCommand):
help = """
Trains the classifier on your data and saves the resulting models to a

View File

@@ -6,20 +6,22 @@ import time
import tqdm
from django.conf import settings
from django.contrib.auth.models import User
from django.core import serializers
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from filelock import FileLock
from documents.models import Document, Correspondent, Tag, DocumentType
from documents.models import Document, Correspondent, Tag, DocumentType, \
SavedView, SavedViewFilterRule
from documents.settings import EXPORTER_FILE_NAME, EXPORTER_THUMBNAIL_NAME, \
EXPORTER_ARCHIVE_NAME
from paperless.db import GnuPG
from paperless_mail.models import MailAccount, MailRule
from ...file_handling import generate_filename, delete_empty_directories
from ...mixins import Renderable
class Command(Renderable, BaseCommand):
class Command(BaseCommand):
help = """
Decrypt and rename all files in our collection into a given target
@@ -55,6 +57,12 @@ class Command(Renderable, BaseCommand):
"do not belong to the current export, such as files from "
"deleted documents."
)
parser.add_argument(
"--no-progress-bar",
default=False,
action="store_true",
help="If set, the progress bar will not be shown"
)
def __init__(self, *args, **kwargs):
BaseCommand.__init__(self, *args, **kwargs)
@@ -79,9 +87,9 @@ class Command(Renderable, BaseCommand):
raise CommandError("That path doesn't appear to be writable")
with FileLock(settings.MEDIA_LOCK):
self.dump()
self.dump(options['no_progress_bar'])
def dump(self):
def dump(self, progress_bar_disable=False):
# 1. Take a snapshot of what files exist in the current export folder
for root, dirs, files in os.walk(self.target):
self.files_in_export_dir.extend(
@@ -106,9 +114,27 @@ class Command(Renderable, BaseCommand):
serializers.serialize("json", documents))
manifest += document_manifest
manifest += json.loads(serializers.serialize(
"json", MailAccount.objects.all()))
manifest += json.loads(serializers.serialize(
"json", MailRule.objects.all()))
manifest += json.loads(serializers.serialize(
"json", SavedView.objects.all()))
manifest += json.loads(serializers.serialize(
"json", SavedViewFilterRule.objects.all()))
manifest += json.loads(serializers.serialize(
"json", User.objects.all()))
# 3. Export files from each document
for index, document_dict in tqdm.tqdm(enumerate(document_manifest),
total=len(document_manifest)):
for index, document_dict in tqdm.tqdm(
enumerate(document_manifest),
total=len(document_manifest),
disable=progress_bar_disable
):
# 3.1. store files unencrypted
document_dict["fields"]["storage_type"] = Document.STORAGE_TYPE_UNENCRYPTED # NOQA: E501
@@ -140,7 +166,7 @@ class Command(Renderable, BaseCommand):
thumbnail_target = os.path.join(self.target, thumbnail_name)
document_dict[EXPORTER_THUMBNAIL_NAME] = thumbnail_name
if os.path.exists(document.archive_path):
if document.has_archive_version:
archive_name = base_name + "-archive.pdf"
archive_target = os.path.join(self.target, archive_name)
document_dict[EXPORTER_ARCHIVE_NAME] = archive_name

View File

@@ -15,7 +15,6 @@ from documents.models import Document
from documents.settings import EXPORTER_FILE_NAME, EXPORTER_THUMBNAIL_NAME, \
EXPORTER_ARCHIVE_NAME
from ...file_handling import create_source_path_directory
from ...mixins import Renderable
from ...signals.handlers import update_filename_and_move_files
@@ -28,7 +27,7 @@ def disable_signal(sig, receiver, sender):
sig.connect(receiver=receiver, sender=sender)
class Command(Renderable, BaseCommand):
class Command(BaseCommand):
help = """
Using a manifest.json file, load the data from there, and import the
@@ -37,6 +36,12 @@ class Command(Renderable, BaseCommand):
def add_arguments(self, parser):
parser.add_argument("source")
parser.add_argument(
"--no-progress-bar",
default=False,
action="store_true",
help="If set, the progress bar will not be shown"
)
def __init__(self, *args, **kwargs):
BaseCommand.__init__(self, *args, **kwargs)
@@ -71,7 +76,7 @@ class Command(Renderable, BaseCommand):
# Fill up the database with whatever is in the manifest
call_command("loaddata", manifest_path)
self._import_files_from_manifest()
self._import_files_from_manifest(options['no_progress_bar'])
print("Updating search index...")
call_command('document_index', 'reindex')
@@ -112,7 +117,7 @@ class Command(Renderable, BaseCommand):
f"does not appear to be in the source directory."
)
def _import_files_from_manifest(self):
def _import_files_from_manifest(self, progress_bar_disable):
os.makedirs(settings.ORIGINALS_DIR, exist_ok=True)
os.makedirs(settings.THUMBNAIL_DIR, exist_ok=True)
@@ -124,7 +129,10 @@ class Command(Renderable, BaseCommand):
lambda r: r["model"] == "documents.document",
self.manifest))
for record in tqdm.tqdm(manifest_documents):
for record in tqdm.tqdm(
manifest_documents,
disable=progress_bar_disable
):
document = Document.objects.get(pk=record["pk"])
@@ -152,6 +160,9 @@ class Command(Renderable, BaseCommand):
shutil.copy2(thumbnail_path, document.thumbnail_path)
if archive_path:
create_source_path_directory(document.archive_path)
# TODO: this assumes that the export is valid and
# archive_filename is present on all documents with
# archived files
shutil.copy2(archive_path, document.archive_path)
document.save()

View File

@@ -1,26 +1,25 @@
from django.core.management import BaseCommand
from django.db import transaction
from documents.mixins import Renderable
from documents.tasks import index_reindex, index_optimize
class Command(Renderable, BaseCommand):
class Command(BaseCommand):
help = "Manages the document index."
def __init__(self, *args, **kwargs):
self.verbosity = 0
BaseCommand.__init__(self, *args, **kwargs)
def add_arguments(self, parser):
parser.add_argument("command", choices=['reindex', 'optimize'])
parser.add_argument(
"--no-progress-bar",
default=False,
action="store_true",
help="If set, the progress bar will not be shown"
)
def handle(self, *args, **options):
self.verbosity = options["verbosity"]
with transaction.atomic():
if options['command'] == 'reindex':
index_reindex()
index_reindex(progress_bar_disable=options['no_progress_bar'])
elif options['command'] == 'optimize':
index_optimize()

View File

@@ -1,12 +0,0 @@
from django.core.management.base import BaseCommand
from documents.models import Log
class Command(BaseCommand):
help = "A quick & dirty way to see what's in the logs"
def handle(self, *args, **options):
for log in Log.objects.order_by("pk"):
print(log)

View File

@@ -5,24 +5,28 @@ from django.core.management.base import BaseCommand
from django.db.models.signals import post_save
from documents.models import Document
from ...mixins import Renderable
class Command(Renderable, BaseCommand):
class Command(BaseCommand):
help = """
This will rename all documents to match the latest filename format.
""".replace(" ", "")
def __init__(self, *args, **kwargs):
self.verbosity = 0
BaseCommand.__init__(self, *args, **kwargs)
def add_arguments(self, parser):
parser.add_argument(
"--no-progress-bar",
default=False,
action="store_true",
help="If set, the progress bar will not be shown"
)
def handle(self, *args, **options):
self.verbosity = options["verbosity"]
logging.getLogger().handlers[0].level = logging.ERROR
for document in tqdm.tqdm(Document.objects.all()):
for document in tqdm.tqdm(
Document.objects.all(),
disable=options['no_progress_bar']
):
post_save.send(Document, instance=document)

64
src/documents/management/commands/document_retagger.py Executable file → Normal file
View File

@@ -1,15 +1,17 @@
import logging
import tqdm
from django.core.management.base import BaseCommand
from documents.classifier import DocumentClassifier, \
IncompatibleClassifierVersionError
from documents.classifier import load_classifier
from documents.models import Document
from ...mixins import Renderable
from ...signals.handlers import set_correspondent, set_document_type, set_tags
class Command(Renderable, BaseCommand):
logger = logging.getLogger("paperless.management.retagger")
class Command(BaseCommand):
help = """
Using the current classification model, assigns correspondents, tags
@@ -18,10 +20,6 @@ class Command(Renderable, BaseCommand):
modified) after their initial import.
""".replace(" ", "")
def __init__(self, *args, **kwargs):
self.verbosity = 0
BaseCommand.__init__(self, *args, **kwargs)
def add_arguments(self, parser):
parser.add_argument(
"-c", "--correspondent",
@@ -59,10 +57,26 @@ class Command(Renderable, BaseCommand):
"set correspondent, document and remove correspondents, types"
"and tags that do not match anymore due to changed rules."
)
parser.add_argument(
"--no-progress-bar",
default=False,
action="store_true",
help="If set, the progress bar will not be shown"
)
parser.add_argument(
"--suggest",
default=False,
action="store_true",
help="Return the suggestion, don't change anything."
)
parser.add_argument(
"--base-url",
help="The base URL to use to build the link to the documents."
)
def handle(self, *args, **options):
self.verbosity = options["verbosity"]
# Detect if we support color
color = self.style.ERROR("test") != "test"
if options["inbox_only"]:
queryset = Document.objects.filter(tags__is_inbox_tag=True)
@@ -70,17 +84,12 @@ class Command(Renderable, BaseCommand):
queryset = Document.objects.all()
documents = queryset.distinct()
classifier = DocumentClassifier()
try:
classifier.reload()
except (OSError, EOFError, IncompatibleClassifierVersionError) as e:
logging.getLogger(__name__).warning(
f"Cannot classify documents: {e}.")
classifier = None
classifier = load_classifier()
for document in documents:
logging.getLogger(__name__).info(
f"Processing document {document.title}")
for document in tqdm.tqdm(
documents,
disable=options['no_progress_bar']
):
if options['correspondent']:
set_correspondent(
@@ -88,18 +97,27 @@ class Command(Renderable, BaseCommand):
document=document,
classifier=classifier,
replace=options['overwrite'],
use_first=options['use_first'])
use_first=options['use_first'],
suggest=options['suggest'],
base_url=options['base_url'],
color=color)
if options['document_type']:
set_document_type(sender=None,
document=document,
classifier=classifier,
replace=options['overwrite'],
use_first=options['use_first'])
use_first=options['use_first'],
suggest=options['suggest'],
base_url=options['base_url'],
color=color)
if options['tags']:
set_tags(
sender=None,
document=document,
classifier=classifier,
replace=options['overwrite'])
replace=options['overwrite'],
suggest=options['suggest'],
base_url=options['base_url'],
color=color)

View File

@@ -0,0 +1,23 @@
from django.core.management.base import BaseCommand
from documents.sanity_checker import check_sanity
class Command(BaseCommand):
help = """
This command checks your document archive for issues.
""".replace(" ", "")
def add_arguments(self, parser):
parser.add_argument(
"--no-progress-bar",
default=False,
action="store_true",
help="If set, the progress bar will not be shown"
)
def handle(self, *args, **options):
messages = check_sanity(progress=not options['no_progress_bar'])
messages.log_messages()

View File

@@ -7,7 +7,6 @@ from django import db
from django.core.management.base import BaseCommand
from documents.models import Document
from ...mixins import Renderable
from ...parsers import get_parser_class_for_mime_type
@@ -23,23 +22,22 @@ def _process_document(doc_in):
try:
thumb = parser.get_optimised_thumbnail(
document.source_path, document.mime_type)
document.source_path,
document.mime_type,
document.get_public_filename()
)
shutil.move(thumb, document.thumbnail_path)
finally:
parser.cleanup()
class Command(Renderable, BaseCommand):
class Command(BaseCommand):
help = """
This will regenerate the thumbnails for all documents.
""".replace(" ", "")
def __init__(self, *args, **kwargs):
self.verbosity = 0
BaseCommand.__init__(self, *args, **kwargs)
def add_arguments(self, parser):
parser.add_argument(
"-d", "--document",
@@ -49,11 +47,14 @@ class Command(Renderable, BaseCommand):
help="Specify the ID of a document, and this command will only "
"run on this specific document."
)
parser.add_argument(
"--no-progress-bar",
default=False,
action="store_true",
help="If set, the progress bar will not be shown"
)
def handle(self, *args, **options):
self.verbosity = options["verbosity"]
logging.getLogger().handlers[0].level = logging.ERROR
if options['document']:
@@ -70,5 +71,7 @@ class Command(Renderable, BaseCommand):
with multiprocessing.Pool() as pool:
list(tqdm.tqdm(
pool.imap_unordered(_process_document, ids), total=len(ids)
pool.imap_unordered(_process_document, ids),
total=len(ids),
disable=options['no_progress_bar']
))

View File

@@ -0,0 +1,42 @@
import logging
import os
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError
logger = logging.getLogger("paperless.management.superuser")
class Command(BaseCommand):
help = """
Creates a Django superuser based on env variables.
""".replace(" ", "")
def handle(self, *args, **options):
username = os.getenv('PAPERLESS_ADMIN_USER')
if not username:
return
mail = os.getenv('PAPERLESS_ADMIN_MAIL', 'root@localhost')
password = os.getenv('PAPERLESS_ADMIN_PASSWORD')
# Check if user exists already, leave as is if it does
if User.objects.filter(username=username).exists():
user: User = User.objects.get_by_natural_key(username)
user.set_password(password)
user.save()
self.stdout.write(f"Changed password of user {username}.")
elif password:
# Create superuser based on env variables
User.objects.create_superuser(username, mail, password)
self.stdout.write(
f'Created superuser "{username}" with provided password.')
else:
self.stdout.write(
f'Did not create superuser "{username}".')
self.stdout.write(
'Make sure you specified "PAPERLESS_ADMIN_PASSWORD" in your '
'"docker-compose.env" file.')

View File

@@ -1,18 +1,17 @@
import logging
import re
from fuzzywuzzy import fuzz
from documents.models import MatchingModel, Correspondent, DocumentType, Tag
logger = logging.getLogger(__name__)
logger = logging.getLogger("paperless.matching")
def log_reason(matching_model, document, reason):
class_name = type(matching_model).__name__
logger.debug(
f"Assigning {class_name} {matching_model.name} to document "
f"{class_name} {matching_model.name} matched on document "
f"{document} because {reason}")
@@ -91,7 +90,7 @@ def matches(matching_model, document):
elif matching_model.matching_algorithm == MatchingModel.MATCH_LITERAL:
result = bool(re.search(
rf"\b{matching_model.match}\b",
rf"\b{re.escape(matching_model.match)}\b",
document_content,
**search_kwargs
))
@@ -123,6 +122,8 @@ def matches(matching_model, document):
return bool(match)
elif matching_model.matching_algorithm == MatchingModel.MATCH_FUZZY:
from fuzzywuzzy import fuzz
match = re.sub(r'[^\w\s]', '', matching_model.match)
text = re.sub(r'[^\w\s]', '', document_content)
if matching_model.is_insensitive:
@@ -160,6 +161,9 @@ def _split_match(matching_model):
findterms = re.compile(r'"([^"]+)"|(\S+)').findall
normspace = re.compile(r"\s+").sub
return [
normspace(" ", (t[0] or t[1]).strip()).replace(" ", r"\s+")
# normspace(" ", (t[0] or t[1]).strip()).replace(" ", r"\s+")
re.escape(
normspace(" ", (t[0] or t[1]).strip())
).replace(r"\ ", r"\s+")
for t in findterms(matching_model.match)
]

View File

@@ -0,0 +1,330 @@
# Generated by Django 3.1.6 on 2021-02-07 22:26
import datetime
import hashlib
import logging
import os
import shutil
from time import sleep
import pathvalidate
from django.conf import settings
from django.db import migrations, models
from django.template.defaultfilters import slugify
from documents.file_handling import defaultdictNoStr, many_to_dictionary
logger = logging.getLogger("paperless.migrations")
###############################################################################
# This is code copied straight paperless before the change.
###############################################################################
def archive_name_from_filename(filename):
return os.path.splitext(filename)[0] + ".pdf"
def archive_path_old(doc):
if doc.filename:
fname = archive_name_from_filename(doc.filename)
else:
fname = "{:07}.pdf".format(doc.pk)
return os.path.join(
settings.ARCHIVE_DIR,
fname
)
STORAGE_TYPE_GPG = "gpg"
def archive_path_new(doc):
if doc.archive_filename is not None:
return os.path.join(
settings.ARCHIVE_DIR,
str(doc.archive_filename)
)
else:
return None
def source_path(doc):
if doc.filename:
fname = str(doc.filename)
else:
fname = "{:07}{}".format(doc.pk, doc.file_type)
if doc.storage_type == STORAGE_TYPE_GPG:
fname += ".gpg" # pragma: no cover
return os.path.join(
settings.ORIGINALS_DIR,
fname
)
def generate_unique_filename(doc, archive_filename=False):
if archive_filename:
old_filename = doc.archive_filename
root = settings.ARCHIVE_DIR
else:
old_filename = doc.filename
root = settings.ORIGINALS_DIR
counter = 0
while True:
new_filename = generate_filename(
doc, counter, archive_filename=archive_filename)
if new_filename == old_filename:
# still the same as before.
return new_filename
if os.path.exists(os.path.join(root, new_filename)):
counter += 1
else:
return new_filename
def generate_filename(doc, counter=0, append_gpg=True, archive_filename=False):
path = ""
try:
if settings.PAPERLESS_FILENAME_FORMAT is not None:
tags = defaultdictNoStr(lambda: slugify(None),
many_to_dictionary(doc.tags))
tag_list = pathvalidate.sanitize_filename(
",".join(sorted(
[tag.name for tag in doc.tags.all()]
)),
replacement_text="-"
)
if doc.correspondent:
correspondent = pathvalidate.sanitize_filename(
doc.correspondent.name, replacement_text="-"
)
else:
correspondent = "none"
if doc.document_type:
document_type = pathvalidate.sanitize_filename(
doc.document_type.name, replacement_text="-"
)
else:
document_type = "none"
path = settings.PAPERLESS_FILENAME_FORMAT.format(
title=pathvalidate.sanitize_filename(
doc.title, replacement_text="-"),
correspondent=correspondent,
document_type=document_type,
created=datetime.date.isoformat(doc.created),
created_year=doc.created.year if doc.created else "none",
created_month=f"{doc.created.month:02}" if doc.created else "none", # NOQA: E501
created_day=f"{doc.created.day:02}" if doc.created else "none",
added=datetime.date.isoformat(doc.added),
added_year=doc.added.year if doc.added else "none",
added_month=f"{doc.added.month:02}" if doc.added else "none",
added_day=f"{doc.added.day:02}" if doc.added else "none",
tags=tags,
tag_list=tag_list
).strip()
path = path.strip(os.sep)
except (ValueError, KeyError, IndexError):
logger.warning(
f"Invalid PAPERLESS_FILENAME_FORMAT: "
f"{settings.PAPERLESS_FILENAME_FORMAT}, falling back to default")
counter_str = f"_{counter:02}" if counter else ""
filetype_str = ".pdf" if archive_filename else doc.file_type
if len(path) > 0:
filename = f"{path}{counter_str}{filetype_str}"
else:
filename = f"{doc.pk:07}{counter_str}{filetype_str}"
# Append .gpg for encrypted files
if append_gpg and doc.storage_type == STORAGE_TYPE_GPG:
filename += ".gpg"
return filename
###############################################################################
# This code performs bidirection archive file transformation.
###############################################################################
def parse_wrapper(parser, path, mime_type, file_name):
# this is here so that I can mock this out for testing.
parser.parse(path, mime_type, file_name)
def create_archive_version(doc, retry_count=3):
from documents.parsers import get_parser_class_for_mime_type, \
DocumentParser, \
ParseError
logger.info(
f"Regenerating archive document for document ID:{doc.id}"
)
parser_class = get_parser_class_for_mime_type(doc.mime_type)
for try_num in range(retry_count):
parser: DocumentParser = parser_class(None, None)
try:
parse_wrapper(parser, source_path(doc), doc.mime_type,
os.path.basename(doc.filename))
doc.content = parser.get_text()
if parser.get_archive_path() and os.path.isfile(
parser.get_archive_path()):
doc.archive_filename = generate_unique_filename(
doc, archive_filename=True)
with open(parser.get_archive_path(), "rb") as f:
doc.archive_checksum = hashlib.md5(f.read()).hexdigest()
os.makedirs(os.path.dirname(archive_path_new(doc)),
exist_ok=True)
shutil.copy2(parser.get_archive_path(), archive_path_new(doc))
else:
doc.archive_checksum = None
logger.error(
f"Parser did not return an archive document for document "
f"ID:{doc.id}. Removing archive document."
)
doc.save()
return
except ParseError:
if try_num + 1 == retry_count:
logger.exception(
f"Unable to regenerate archive document for ID:{doc.id}. You "
f"need to invoke the document_archiver management command "
f"manually for that document."
)
doc.archive_checksum = None
doc.save()
return
else:
# This is mostly here for the tika parser in docker
# environemnts. The servers for parsing need to come up first,
# and the docker setup doesn't ensure that tika is running
# before attempting migrations.
logger.error("Parse error, will try again in 5 seconds...")
sleep(5)
finally:
parser.cleanup()
def move_old_to_new_locations(apps, schema_editor):
Document = apps.get_model("documents", "Document")
affected_document_ids = set()
old_archive_path_to_id = {}
# check for documents that have incorrect archive versions
for doc in Document.objects.filter(archive_checksum__isnull=False):
old_path = archive_path_old(doc)
if old_path in old_archive_path_to_id:
affected_document_ids.add(doc.id)
affected_document_ids.add(old_archive_path_to_id[old_path])
else:
old_archive_path_to_id[old_path] = doc.id
# check that archive files of all unaffected documents are in place
for doc in Document.objects.filter(archive_checksum__isnull=False):
old_path = archive_path_old(doc)
if doc.id not in affected_document_ids and not os.path.isfile(old_path):
raise ValueError(
f"Archived document ID:{doc.id} does not exist at: "
f"{old_path}")
# check that we can regenerate affected archive versions
for doc_id in affected_document_ids:
from documents.parsers import get_parser_class_for_mime_type
doc = Document.objects.get(id=doc_id)
parser_class = get_parser_class_for_mime_type(doc.mime_type)
if not parser_class:
raise ValueError(
f"Document ID:{doc.id} has an invalid archived document, "
f"but no parsers are available. Cannot migrate.")
for doc in Document.objects.filter(archive_checksum__isnull=False):
if doc.id in affected_document_ids:
old_path = archive_path_old(doc)
# remove affected archive versions
if os.path.isfile(old_path):
logger.debug(
f"Removing {old_path}"
)
os.unlink(old_path)
else:
# Set archive path for unaffected files
doc.archive_filename = archive_name_from_filename(doc.filename)
Document.objects.filter(id=doc.id).update(
archive_filename=doc.archive_filename
)
# regenerate archive documents
for doc_id in affected_document_ids:
doc = Document.objects.get(id=doc_id)
create_archive_version(doc)
def move_new_to_old_locations(apps, schema_editor):
Document = apps.get_model("documents", "Document")
old_archive_paths = set()
for doc in Document.objects.filter(archive_checksum__isnull=False):
new_archive_path = archive_path_new(doc)
old_archive_path = archive_path_old(doc)
if old_archive_path in old_archive_paths:
raise ValueError(
f"Cannot migrate: Archive file name {old_archive_path} of "
f"document {doc.filename} would clash with another archive "
f"filename.")
old_archive_paths.add(old_archive_path)
if new_archive_path != old_archive_path and os.path.isfile(old_archive_path):
raise ValueError(
f"Cannot migrate: Cannot move {new_archive_path} to "
f"{old_archive_path}: file already exists."
)
for doc in Document.objects.filter(archive_checksum__isnull=False):
new_archive_path = archive_path_new(doc)
old_archive_path = archive_path_old(doc)
if new_archive_path != old_archive_path:
logger.debug(f"Moving {new_archive_path} to {old_archive_path}")
shutil.move(new_archive_path, old_archive_path)
class Migration(migrations.Migration):
dependencies = [
('documents', '1011_auto_20210101_2340'),
]
operations = [
migrations.AddField(
model_name='document',
name='archive_filename',
field=models.FilePathField(default=None, editable=False, help_text='Current archive filename in storage', max_length=1024, null=True, unique=True, verbose_name='archive filename'),
),
migrations.AlterField(
model_name='document',
name='filename',
field=models.FilePathField(default=None, editable=False, help_text='Current filename in storage', max_length=1024, null=True, unique=True, verbose_name='filename'),
),
migrations.RunPython(
move_old_to_new_locations,
move_new_to_old_locations
),
]

View File

@@ -0,0 +1,70 @@
# Generated by Django 3.1.4 on 2020-12-02 21:43
from django.db import migrations, models
COLOURS_OLD = {
1: "#a6cee3",
2: "#1f78b4",
3: "#b2df8a",
4: "#33a02c",
5: "#fb9a99",
6: "#e31a1c",
7: "#fdbf6f",
8: "#ff7f00",
9: "#cab2d6",
10: "#6a3d9a",
11: "#b15928",
12: "#000000",
13: "#cccccc",
}
def forward(apps, schema_editor):
Tag = apps.get_model('documents', 'Tag')
for tag in Tag.objects.all():
colour_old_id = tag.colour_old
rgb = COLOURS_OLD[colour_old_id]
tag.color = rgb
tag.save()
def reverse(apps, schema_editor):
Tag = apps.get_model('documents', 'Tag')
def _get_colour_id(rdb):
for idx, rdbx in COLOURS_OLD.items():
if rdbx == rdb:
return idx
# Return colour 1 if we can't match anything
return 1
for tag in Tag.objects.all():
colour_id = _get_colour_id(tag.color)
tag.colour_old = colour_id
tag.save()
class Migration(migrations.Migration):
dependencies = [
('documents', '1012_fix_archive_files'),
]
operations = [
migrations.RenameField(
model_name='tag',
old_name='colour',
new_name='colour_old',
),
migrations.AddField(
model_name='tag',
name='color',
field=models.CharField(default='#a6cee3', max_length=7, verbose_name='color'),
),
migrations.RunPython(forward, reverse),
migrations.RemoveField(
model_name='tag',
name='colour_old',
)
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-02-28 15:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('documents', '1013_migrate_tag_colour'),
]
operations = [
migrations.AlterField(
model_name='savedviewfilterrule',
name='rule_type',
field=models.PositiveIntegerField(choices=[(0, 'title contains'), (1, 'content contains'), (2, 'ASN is'), (3, 'correspondent is'), (4, 'document type is'), (5, 'is in inbox'), (6, 'has tag'), (7, 'has any tag'), (8, 'created before'), (9, 'created after'), (10, 'created year is'), (11, 'created month is'), (12, 'created day is'), (13, 'added before'), (14, 'added after'), (15, 'modified before'), (16, 'modified after'), (17, 'does not have tag'), (18, 'does not have ASN'), (19, 'title or content contains')], verbose_name='rule type'),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.1.7 on 2021-04-04 18:28
import logging
from django.db import migrations
logger = logging.getLogger("paperless.migrations")
def remove_null_characters(apps, schema_editor):
Document = apps.get_model('documents', 'Document')
for doc in Document.objects.all():
content: str = doc.content
if '\0' in content:
logger.info(f"Removing null characters from document {doc}...")
doc.content = content.replace('\0', ' ')
doc.save()
class Migration(migrations.Migration):
dependencies = [
('documents', '1014_auto_20210228_1614'),
]
operations = [
migrations.RunPython(remove_null_characters, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.7 on 2021-03-17 12:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('documents', '1015_remove_null_characters'),
]
operations = [
migrations.AlterField(
model_name='savedview',
name='sort_field',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='sort field'),
),
migrations.AlterField(
model_name='savedviewfilterrule',
name='rule_type',
field=models.PositiveIntegerField(choices=[(0, 'title contains'), (1, 'content contains'), (2, 'ASN is'), (3, 'correspondent is'), (4, 'document type is'), (5, 'is in inbox'), (6, 'has tag'), (7, 'has any tag'), (8, 'created before'), (9, 'created after'), (10, 'created year is'), (11, 'created month is'), (12, 'created day is'), (13, 'added before'), (14, 'added after'), (15, 'modified before'), (16, 'modified after'), (17, 'does not have tag'), (18, 'does not have ASN'), (19, 'title or content contains'), (20, 'fulltext query'), (21, 'more like this')], verbose_name='rule type'),
),
]

View File

@@ -1,9 +0,0 @@
class Renderable:
"""
A handy mixin to make it easier/cleaner to print output based on a
verbosity value.
"""
def _render(self, text, verbosity):
if self.verbosity >= verbosity:
print(text)

69
src/documents/models.py Executable file → Normal file
View File

@@ -16,7 +16,6 @@ from django.utils.timezone import is_aware
from django.utils.translation import gettext_lazy as _
from documents.file_handling import archive_name_from_filename
from documents.parsers import get_default_file_extension
@@ -66,10 +65,6 @@ class MatchingModel(models.Model):
class Correspondent(MatchingModel):
# This regex is probably more restrictive than it needs to be, but it's
# better safe than sorry.
SAFE_REGEX = re.compile(r"^[\w\- ,.']+$")
class Meta:
ordering = ("name",)
verbose_name = _("correspondent")
@@ -78,25 +73,11 @@ class Correspondent(MatchingModel):
class Tag(MatchingModel):
COLOURS = (
(1, "#a6cee3"),
(2, "#1f78b4"),
(3, "#b2df8a"),
(4, "#33a02c"),
(5, "#fb9a99"),
(6, "#e31a1c"),
(7, "#fdbf6f"),
(8, "#ff7f00"),
(9, "#cab2d6"),
(10, "#6a3d9a"),
(11, "#b15928"),
(12, "#000000"),
(13, "#cccccc")
)
colour = models.PositiveIntegerField(
color = models.CharField(
_("color"),
choices=COLOURS, default=1)
max_length=7,
default="#a6cee3"
)
is_inbox_tag = models.BooleanField(
_("is inbox tag"),
@@ -208,10 +189,21 @@ class Document(models.Model):
max_length=1024,
editable=False,
default=None,
unique=True,
null=True,
help_text=_("Current filename in storage")
)
archive_filename = models.FilePathField(
_("archive filename"),
max_length=1024,
editable=False,
default=None,
unique=True,
null=True,
help_text=_("Current archive filename in storage")
)
archive_serial_number = models.IntegerField(
_("archive serial number"),
blank=True,
@@ -256,16 +248,18 @@ class Document(models.Model):
return open(self.source_path, "rb")
@property
def archive_path(self):
if self.filename:
fname = archive_name_from_filename(self.filename)
else:
fname = "{:07}.pdf".format(self.pk)
def has_archive_version(self):
return self.archive_filename is not None
return os.path.join(
settings.ARCHIVE_DIR,
fname
)
@property
def archive_path(self):
if self.has_archive_version:
return os.path.join(
settings.ARCHIVE_DIR,
str(self.archive_filename)
)
else:
return None
@property
def archive_file(self):
@@ -361,7 +355,10 @@ class SavedView(models.Model):
sort_field = models.CharField(
_("sort field"),
max_length=128)
max_length=128,
null=True,
blank=True
)
sort_reverse = models.BooleanField(
_("sort reverse"),
default=False)
@@ -387,7 +384,11 @@ class SavedViewFilterRule(models.Model):
(15, _("modified before")),
(16, _("modified after")),
(17, _("does not have tag")),
(19, _("has tags in")),
(18, _("does not have ASN")),
(19, _("title or content contains")),
(20, _("fulltext query")),
(21, _("more like this")),
(22, _("has tags in"))
]
saved_view = models.ForeignKey(

View File

@@ -6,7 +6,6 @@ import shutil
import subprocess
import tempfile
import dateparser
import magic
from django.conf import settings
from django.utils import timezone
@@ -36,7 +35,7 @@ DATE_REGEX = re.compile(
)
logger = logging.getLogger(__name__)
logger = logging.getLogger("paperless.parsing")
def is_mime_type_supported(mime_type):
@@ -144,6 +143,46 @@ def run_convert(input_file,
raise ParseError("Convert failed at {}".format(args))
def get_default_thumbnail():
return os.path.join(os.path.dirname(__file__), "resources", "document.png")
def make_thumbnail_from_pdf_gs_fallback(in_path, temp_dir, logging_group=None):
out_path = os.path.join(temp_dir, "convert_gs.png")
# if convert fails, fall back to extracting
# the first PDF page as a PNG using Ghostscript
logger.warning(
"Thumbnail generation with ImageMagick failed, falling back "
"to ghostscript. Check your /etc/ImageMagick-x/policy.xml!",
extra={'group': logging_group}
)
gs_out_path = os.path.join(temp_dir, "gs_out.png")
cmd = [settings.GS_BINARY,
"-q",
"-sDEVICE=pngalpha",
"-o", gs_out_path,
in_path]
try:
if not subprocess.Popen(cmd).wait() == 0:
raise ParseError("Thumbnail (gs) failed at {}".format(cmd))
# then run convert on the output from gs
run_convert(density=300,
scale="500x5000>",
alpha="remove",
strip=True,
trim=False,
auto_orient=True,
input_file=gs_out_path,
output_file=out_path,
logging_group=logging_group)
return out_path
except ParseError:
return get_default_thumbnail()
def make_thumbnail_from_pdf(in_path, temp_dir, logging_group=None):
"""
The thumbnail of a PDF is just a 500px wide image of the first page.
@@ -162,31 +201,8 @@ def make_thumbnail_from_pdf(in_path, temp_dir, logging_group=None):
output_file=out_path,
logging_group=logging_group)
except ParseError:
# if convert fails, fall back to extracting
# the first PDF page as a PNG using Ghostscript
logger.warning(
"Thumbnail generation with ImageMagick failed, falling back "
"to ghostscript. Check your /etc/ImageMagick-x/policy.xml!",
extra={'group': logging_group}
)
gs_out_path = os.path.join(temp_dir, "gs_out.png")
cmd = [settings.GS_BINARY,
"-q",
"-sDEVICE=pngalpha",
"-o", gs_out_path,
in_path]
if not subprocess.Popen(cmd).wait() == 0:
raise ParseError("Thumbnail (gs) failed at {}".format(cmd))
# then run convert on the output from gs
run_convert(density=300,
scale="500x5000>",
alpha="remove",
strip=True,
trim=False,
auto_orient=True,
input_file=gs_out_path,
output_file=out_path,
logging_group=logging_group)
out_path = make_thumbnail_from_pdf_gs_fallback(
in_path, temp_dir, logging_group)
return out_path
@@ -200,6 +216,8 @@ def parse_date(filename, text):
"""
Call dateparser.parse with a particular date ordering
"""
import dateparser
return dateparser.parse(
ds,
settings={
@@ -261,7 +279,9 @@ class DocumentParser(LoggingMixin):
`paperless_tesseract.parsers` for inspiration.
"""
def __init__(self, logging_group):
logging_name = "paperless.parsing"
def __init__(self, logging_group, progress_callback=None):
super().__init__()
self.logging_group = logging_group
os.makedirs(settings.SCRATCH_DIR, exist_ok=True)
@@ -271,6 +291,11 @@ class DocumentParser(LoggingMixin):
self.archive_path = None
self.text = None
self.date = None
self.progress_callback = progress_callback
def progress(self, current_progress, max_progress):
if self.progress_callback:
self.progress_callback(current_progress, max_progress)
def extract_metadata(self, document_path, mime_type):
return []
@@ -281,14 +306,17 @@ class DocumentParser(LoggingMixin):
def get_archive_path(self):
return self.archive_path
def get_thumbnail(self, document_path, mime_type):
def get_thumbnail(self, document_path, mime_type, file_name=None):
"""
Returns the path to a file we can use as a thumbnail for this document.
"""
raise NotImplementedError()
def get_optimised_thumbnail(self, document_path, mime_type):
thumbnail = self.get_thumbnail(document_path, mime_type)
def get_optimised_thumbnail(self,
document_path,
mime_type,
file_name=None):
thumbnail = self.get_thumbnail(document_path, mime_type, file_name)
if settings.OPTIMIZE_THUMBNAILS:
out_path = os.path.join(self.tempdir, "thumb_optipng.png")
@@ -311,5 +339,5 @@ class DocumentParser(LoggingMixin):
return self.date
def cleanup(self):
self.log("debug", "Deleting directory {}".format(self.tempdir))
self.log("debug", f"Deleting directory {self.tempdir}")
shutil.rmtree(self.tempdir)

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,45 +1,55 @@
import hashlib
import logging
import os
from django.conf import settings
from tqdm import tqdm
from documents.models import Document
class SanityMessage:
message = None
class SanityCheckMessages:
def __init__(self):
self._messages = []
def error(self, message):
self._messages.append({"level": logging.ERROR, "message": message})
def warning(self, message):
self._messages.append({"level": logging.WARNING, "message": message})
def info(self, message):
self._messages.append({"level": logging.INFO, "message": message})
def log_messages(self):
logger = logging.getLogger("paperless.sanity_checker")
if len(self._messages) == 0:
logger.info("Sanity checker detected no issues.")
else:
for msg in self._messages:
logger.log(msg['level'], msg['message'])
def __len__(self):
return len(self._messages)
def __getitem__(self, item):
return self._messages[item]
def has_error(self):
return any([msg['level'] == logging.ERROR for msg in self._messages])
def has_warning(self):
return any([msg['level'] == logging.WARNING for msg in self._messages])
class SanityWarning(SanityMessage):
def __init__(self, message):
self.message = message
def __str__(self):
return f"Warning: {self.message}"
class SanityCheckFailedException(Exception):
pass
class SanityError(SanityMessage):
def __init__(self, message):
self.message = message
def __str__(self):
return f"ERROR: {self.message}"
class SanityFailedError(Exception):
def __init__(self, messages):
self.messages = messages
def __str__(self):
message_string = "\n".join([str(m) for m in self.messages])
return (
f"The following issuse were found by the sanity checker:\n"
f"{message_string}\n\n===============\n\n")
def check_sanity():
messages = []
def check_sanity(progress=False):
messages = SanityCheckMessages()
present_files = []
for root, subdirs, files in os.walk(settings.MEDIA_ROOT):
@@ -50,72 +60,81 @@ def check_sanity():
if lockfile in present_files:
present_files.remove(lockfile)
for doc in Document.objects.all():
for doc in tqdm(Document.objects.all(), disable=not progress):
# Check sanity of the thumbnail
if not os.path.isfile(doc.thumbnail_path):
messages.append(SanityError(
f"Thumbnail of document {doc.pk} does not exist."))
messages.error(f"Thumbnail of document {doc.pk} does not exist.")
else:
present_files.remove(os.path.normpath(doc.thumbnail_path))
if os.path.normpath(doc.thumbnail_path) in present_files:
present_files.remove(os.path.normpath(doc.thumbnail_path))
try:
with doc.thumbnail_file as f:
f.read()
except OSError as e:
messages.append(SanityError(
messages.error(
f"Cannot read thumbnail file of document {doc.pk}: {e}"
))
)
# Check sanity of the original file
# TODO: extract method
if not os.path.isfile(doc.source_path):
messages.append(SanityError(
f"Original of document {doc.pk} does not exist."))
messages.error(f"Original of document {doc.pk} does not exist.")
else:
present_files.remove(os.path.normpath(doc.source_path))
if os.path.normpath(doc.source_path) in present_files:
present_files.remove(os.path.normpath(doc.source_path))
try:
with doc.source_file as f:
checksum = hashlib.md5(f.read()).hexdigest()
except OSError as e:
messages.append(SanityError(
f"Cannot read original file of document {doc.pk}: {e}"))
messages.error(
f"Cannot read original file of document {doc.pk}: {e}")
else:
if not checksum == doc.checksum:
messages.append(SanityError(
messages.error(
f"Checksum mismatch of document {doc.pk}. "
f"Stored: {doc.checksum}, actual: {checksum}."
))
)
# Check sanity of the archive file.
if doc.archive_checksum:
if doc.archive_checksum and not doc.archive_filename:
messages.error(
f"Document {doc.pk} has an archive file checksum, but no "
f"archive filename."
)
elif not doc.archive_checksum and doc.archive_filename:
messages.error(
f"Document {doc.pk} has an archive file, but its checksum is "
f"missing."
)
elif doc.has_archive_version:
if not os.path.isfile(doc.archive_path):
messages.append(SanityError(
messages.error(
f"Archived version of document {doc.pk} does not exist."
))
)
else:
present_files.remove(os.path.normpath(doc.archive_path))
if os.path.normpath(doc.archive_path) in present_files:
present_files.remove(os.path.normpath(doc.archive_path))
try:
with doc.archive_file as f:
checksum = hashlib.md5(f.read()).hexdigest()
except OSError as e:
messages.append(SanityError(
messages.error(
f"Cannot read archive file of document {doc.pk}: {e}"
))
)
else:
if not checksum == doc.archive_checksum:
messages.append(SanityError(
f"Checksum mismatch of archive {doc.pk}. "
f"Stored: {doc.checksum}, actual: {checksum}."
))
messages.error(
f"Checksum mismatch of archived document "
f"{doc.pk}. "
f"Stored: {doc.archive_checksum}, "
f"actual: {checksum}."
)
# other document checks
if not doc.content:
messages.append(SanityWarning(
f"Document {doc.pk} has no content."
))
messages.info(f"Document {doc.pk} has no content.")
for extra_file in present_files:
messages.append(SanityWarning(
f"Orphaned file in media dir: {extra_file}"
))
messages.warning(f"Orphaned file in media dir: {extra_file}")
return messages

View File

@@ -1,13 +1,18 @@
import re
import magic
import math
from django.utils.text import slugify
from rest_framework import serializers
from rest_framework.fields import SerializerMethodField
from . import bulk_edit
from .models import Correspondent, Tag, Document, Log, DocumentType, \
SavedView, SavedViewFilterRule
from .models import Correspondent, Tag, Document, DocumentType, \
SavedView, SavedViewFilterRule, MatchingModel
from .parsers import is_mime_type_supported
from django.utils.translation import gettext as _
# https://www.django-rest-framework.org/api-guide/serializers/#example
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
@@ -31,16 +36,30 @@ class DynamicFieldsModelSerializer(serializers.ModelSerializer):
self.fields.pop(field_name)
class CorrespondentSerializer(serializers.ModelSerializer):
class MatchingModelSerializer(serializers.ModelSerializer):
document_count = serializers.IntegerField(read_only=True)
last_correspondence = serializers.DateTimeField(read_only=True)
def get_slug(self, obj):
return slugify(obj.name)
slug = SerializerMethodField()
def validate_match(self, match):
if 'matching_algorithm' in self.initial_data and self.initial_data['matching_algorithm'] == MatchingModel.MATCH_REGEX: # NOQA: E501
try:
re.compile(match)
except Exception as e:
raise serializers.ValidationError(
_("Invalid regular expression: %(error)s") %
{'error': str(e)}
)
return match
class CorrespondentSerializer(MatchingModelSerializer):
last_correspondence = serializers.DateTimeField(read_only=True)
class Meta:
model = Correspondent
fields = (
@@ -55,13 +74,7 @@ class CorrespondentSerializer(serializers.ModelSerializer):
)
class DocumentTypeSerializer(serializers.ModelSerializer):
document_count = serializers.IntegerField(read_only=True)
def get_slug(self, obj):
return slugify(obj.name)
slug = SerializerMethodField()
class DocumentTypeSerializer(MatchingModelSerializer):
class Meta:
model = DocumentType
@@ -76,13 +89,40 @@ class DocumentTypeSerializer(serializers.ModelSerializer):
)
class TagSerializer(serializers.ModelSerializer):
class ColorField(serializers.Field):
document_count = serializers.IntegerField(read_only=True)
COLOURS = (
(1, "#a6cee3"),
(2, "#1f78b4"),
(3, "#b2df8a"),
(4, "#33a02c"),
(5, "#fb9a99"),
(6, "#e31a1c"),
(7, "#fdbf6f"),
(8, "#ff7f00"),
(9, "#cab2d6"),
(10, "#6a3d9a"),
(11, "#b15928"),
(12, "#000000"),
(13, "#cccccc")
)
def get_slug(self, obj):
return slugify(obj.name)
slug = SerializerMethodField()
def to_internal_value(self, data):
for id, color in self.COLOURS:
if id == data:
return color
raise serializers.ValidationError()
def to_representation(self, value):
for id, color in self.COLOURS:
if color == value:
return id
return 1
class TagSerializerVersion1(MatchingModelSerializer):
colour = ColorField(source='color', default="#a6cee3")
class Meta:
model = Tag
@@ -99,6 +139,45 @@ class TagSerializer(serializers.ModelSerializer):
)
class TagSerializer(MatchingModelSerializer):
def get_text_color(self, obj):
try:
h = obj.color.lstrip('#')
rgb = tuple(int(h[i:i + 2], 16)/256 for i in (0, 2, 4))
luminance = math.sqrt(
0.299 * math.pow(rgb[0], 2) +
0.587 * math.pow(rgb[1], 2) +
0.114 * math.pow(rgb[2], 2)
)
return "#ffffff" if luminance < 0.53 else "#000000"
except ValueError:
return "#000000"
text_color = serializers.SerializerMethodField()
class Meta:
model = Tag
fields = (
"id",
"slug",
"name",
"color",
"text_color",
"match",
"matching_algorithm",
"is_insensitive",
"is_inbox_tag",
"document_count"
)
def validate_color(self, color):
regex = r"#[0-9a-fA-F]{6}"
if not re.match(regex, color):
raise serializers.ValidationError(_("Invalid color."))
return color
class CorrespondentField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
return Correspondent.objects.all()
@@ -127,7 +206,7 @@ class DocumentSerializer(DynamicFieldsModelSerializer):
return obj.get_public_filename()
def get_archived_file_name(self, obj):
if obj.archive_checksum:
if obj.has_archive_version:
return obj.get_public_filename(archive=True)
else:
return None
@@ -151,19 +230,6 @@ class DocumentSerializer(DynamicFieldsModelSerializer):
)
class LogSerializer(serializers.ModelSerializer):
class Meta:
model = Log
fields = (
"id",
"created",
"message",
"group",
"level"
)
class SavedViewFilterRuleSerializer(serializers.ModelSerializer):
class Meta:
@@ -203,14 +269,34 @@ class SavedViewSerializer(serializers.ModelSerializer):
return saved_view
class BulkEditSerializer(serializers.Serializer):
class DocumentListSerializer(serializers.Serializer):
documents = serializers.ListField(
child=serializers.IntegerField(),
required=True,
label="Documents",
write_only=True
write_only=True,
child=serializers.IntegerField()
)
def _validate_document_id_list(self, documents, name="documents"):
if not type(documents) == list:
raise serializers.ValidationError(f"{name} must be a list")
if not all([type(i) == int for i in documents]):
raise serializers.ValidationError(
f"{name} must be a list of integers")
count = Document.objects.filter(id__in=documents).count()
if not count == len(documents):
raise serializers.ValidationError(
f"Some documents in {name} don't exist or were "
f"specified twice.")
def validate_documents(self, documents):
self._validate_document_id_list(documents)
return documents
class BulkEditSerializer(DocumentListSerializer):
method = serializers.ChoiceField(
choices=[
"set_correspondent",
@@ -226,18 +312,6 @@ class BulkEditSerializer(serializers.Serializer):
parameters = serializers.DictField(allow_empty=True)
def _validate_document_id_list(self, documents, name="documents"):
if not type(documents) == list:
raise serializers.ValidationError(f"{name} must be a list")
if not all([type(i) == int for i in documents]):
raise serializers.ValidationError(
f"{name} must be a list of integers")
count = Document.objects.filter(id__in=documents).count()
if not count == len(documents):
raise serializers.ValidationError(
f"Some documents in {name} don't exist or were "
f"specified twice.")
def _validate_tag_id_list(self, tags, name="tags"):
if not type(tags) == list:
raise serializers.ValidationError(f"{name} must be a list")
@@ -249,10 +323,6 @@ class BulkEditSerializer(serializers.Serializer):
raise serializers.ValidationError(
f"Some tags in {name} don't exist or were specified twice.")
def validate_documents(self, documents):
self._validate_document_id_list(documents)
return documents
def validate_method(self, method):
if method == "set_correspondent":
return bulk_edit.set_correspondent
@@ -378,7 +448,9 @@ class PostDocumentSerializer(serializers.Serializer):
if not is_mime_type_supported(mime_type):
raise serializers.ValidationError(
"This file type is not supported.")
_("File type %(type)s not supported") %
{'type': mime_type}
)
return document.name, document_data
@@ -401,9 +473,24 @@ class PostDocumentSerializer(serializers.Serializer):
return None
class SelectionDataSerializer(serializers.Serializer):
class BulkDownloadSerializer(DocumentListSerializer):
documents = serializers.ListField(
required=True,
child=serializers.IntegerField()
content = serializers.ChoiceField(
choices=["archive", "originals", "both"],
default="archive"
)
compression = serializers.ChoiceField(
choices=["none", "deflated", "bzip2", "lzma"],
default="none"
)
def validate_compression(self, compression):
import zipfile
return {
"none": zipfile.ZIP_STORED,
"deflated": zipfile.ZIP_DEFLATED,
"bzip2": zipfile.ZIP_BZIP2,
"lzma": zipfile.ZIP_LZMA
}[compression]

284
src/documents/signals/handlers.py Executable file → Normal file
View File

@@ -1,7 +1,7 @@
import logging
import os
from subprocess import Popen
from django.utils import termcolors
from django.conf import settings
from django.contrib.admin.models import ADDITION, LogEntry
from django.contrib.auth.models import User
@@ -9,18 +9,17 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models, DatabaseError
from django.db.models import Q
from django.dispatch import receiver
from django.utils import timezone
from django.utils import termcolors, timezone
from filelock import FileLock
from .. import index, matching
from .. import matching
from ..file_handling import delete_empty_directories, \
create_source_path_directory, archive_name_from_filename, \
create_source_path_directory, \
generate_unique_filename
from ..models import Document, Tag
from ..models import Document, Tag, MatchingModel
def logger(message, group):
logging.getLogger(__name__).debug(message, extra={"group": group})
logger = logging.getLogger("paperless.handlers")
def add_inbox_tags(sender, document=None, logging_group=None, **kwargs):
@@ -34,6 +33,9 @@ def set_correspondent(sender,
classifier=None,
replace=False,
use_first=True,
suggest=False,
base_url=None,
color=False,
**kwargs):
if document.correspondent and not replace:
return
@@ -48,27 +50,45 @@ def set_correspondent(sender,
selected = None
if potential_count > 1:
if use_first:
logger(
logger.debug(
f"Detected {potential_count} potential correspondents, "
f"so we've opted for {selected}",
logging_group
extra={'group': logging_group}
)
else:
logger(
logger.debug(
f"Detected {potential_count} potential correspondents, "
f"not assigning any correspondent",
logging_group
extra={'group': logging_group}
)
return
if selected or replace:
logger(
f"Assigning correspondent {selected} to {document}",
logging_group
)
if suggest:
if base_url:
print(
termcolors.colorize(str(document), fg='green')
if color
else str(document)
)
print(f"{base_url}/documents/{document.pk}")
else:
print(
(
termcolors.colorize(str(document), fg='green')
if color
else str(document)
) + f" [{document.pk}]"
)
print(f"Suggest correspondent {selected}")
else:
logger.info(
f"Assigning correspondent {selected} to {document}",
extra={'group': logging_group}
)
document.correspondent = selected
document.save(update_fields=("correspondent",))
document.correspondent = selected
document.save(update_fields=("correspondent",))
def set_document_type(sender,
@@ -77,6 +97,9 @@ def set_document_type(sender,
classifier=None,
replace=False,
use_first=True,
suggest=False,
base_url=None,
color=False,
**kwargs):
if document.document_type and not replace:
return
@@ -92,27 +115,45 @@ def set_document_type(sender,
if potential_count > 1:
if use_first:
logger(
logger.info(
f"Detected {potential_count} potential document types, "
f"so we've opted for {selected}",
logging_group
extra={'group': logging_group}
)
else:
logger(
logger.info(
f"Detected {potential_count} potential document types, "
f"not assigning any document type",
logging_group
extra={'group': logging_group}
)
return
if selected or replace:
logger(
f"Assigning document type {selected} to {document}",
logging_group
)
if suggest:
if base_url:
print(
termcolors.colorize(str(document), fg='green')
if color
else str(document)
)
print(f"{base_url}/documents/{document.pk}")
else:
print(
(
termcolors.colorize(str(document), fg='green')
if color
else str(document)
) + f" [{document.pk}]"
)
print(f"Sugest document type {selected}")
else:
logger.info(
f"Assigning document type {selected} to {document}",
extra={'group': logging_group}
)
document.document_type = selected
document.save(update_fields=("document_type",))
document.document_type = selected
document.save(update_fields=("document_type",))
def set_tags(sender,
@@ -120,6 +161,9 @@ def set_tags(sender,
logging_group=None,
classifier=None,
replace=False,
suggest=False,
base_url=None,
color=False,
**kwargs):
if replace:
@@ -134,33 +178,65 @@ def set_tags(sender,
relevant_tags = set(matched_tags) - current_tags
if not relevant_tags:
return
if suggest:
extra_tags = current_tags - set(matched_tags)
extra_tags = [
t for t in extra_tags
if t.matching_algorithm == MatchingModel.MATCH_AUTO
]
if not relevant_tags and not extra_tags:
return
if base_url:
print(
termcolors.colorize(str(document), fg='green')
if color
else str(document)
)
print(f"{base_url}/documents/{document.pk}")
else:
print(
(
termcolors.colorize(str(document), fg='green')
if color
else str(document)
) + f" [{document.pk}]"
)
if relevant_tags:
print(
"Suggest tags: " + ", ".join([t.name for t in relevant_tags])
)
if extra_tags:
print("Extra tags: " + ", ".join([t.name for t in extra_tags]))
else:
if not relevant_tags:
return
message = 'Tagging "{}" with "{}"'
logger(
message.format(document, ", ".join([t.name for t in relevant_tags])),
logging_group
)
message = 'Tagging "{}" with "{}"'
logger.info(
message.format(
document, ", ".join([t.name for t in relevant_tags])
),
extra={'group': logging_group}
)
document.tags.add(*relevant_tags)
document.tags.add(*relevant_tags)
@receiver(models.signals.post_delete, sender=Document)
def cleanup_document_deletion(sender, instance, using, **kwargs):
with FileLock(settings.MEDIA_LOCK):
for f in (instance.source_path,
instance.archive_path,
instance.thumbnail_path):
if os.path.isfile(f):
for filename in (instance.source_path,
instance.archive_path,
instance.thumbnail_path):
if filename and os.path.isfile(filename):
try:
os.unlink(f)
logging.getLogger(__name__).debug(
f"Deleted file {f}.")
os.unlink(filename)
logger.debug(
f"Deleted file {filename}.")
except OSError as e:
logging.getLogger(__name__).warning(
logger.warning(
f"While deleting document {str(instance)}, the file "
f"{f} could not be deleted: {e}"
f"{filename} could not be deleted: {e}"
)
delete_empty_directories(
@@ -168,27 +244,30 @@ def cleanup_document_deletion(sender, instance, using, **kwargs):
root=settings.ORIGINALS_DIR
)
delete_empty_directories(
os.path.dirname(instance.archive_path),
root=settings.ARCHIVE_DIR
)
if instance.has_archive_version:
delete_empty_directories(
os.path.dirname(instance.archive_path),
root=settings.ARCHIVE_DIR
)
class CannotMoveFilesException(Exception):
pass
def validate_move(instance, old_path, new_path):
if not os.path.isfile(old_path):
# Can't do anything if the old file does not exist anymore.
logging.getLogger(__name__).fatal(
logger.fatal(
f"Document {str(instance)}: File {old_path} has gone.")
return False
raise CannotMoveFilesException()
if os.path.isfile(new_path):
# Can't do anything if the new file already exists. Skip updating file.
logging.getLogger(__name__).warning(
logger.warning(
f"Document {str(instance)}: Cannot rename file "
f"since target path {new_path} already exists.")
return False
return True
raise CannotMoveFilesException()
@receiver(models.signals.m2m_changed, sender=Document.tags.through)
@@ -207,56 +286,61 @@ def update_filename_and_move_files(sender, instance, **kwargs):
return
with FileLock(settings.MEDIA_LOCK):
old_filename = instance.filename
new_filename = generate_unique_filename(
instance, settings.ORIGINALS_DIR)
try:
old_filename = instance.filename
old_source_path = instance.source_path
if new_filename == instance.filename:
# Don't do anything if its the same.
return
instance.filename = generate_unique_filename(instance)
move_original = old_filename != instance.filename
old_source_path = instance.source_path
new_source_path = os.path.join(settings.ORIGINALS_DIR, new_filename)
if not validate_move(instance, old_source_path, new_source_path):
return
# archive files are optional, archive checksum tells us if we have one,
# since this is None for documents without archived files.
if instance.archive_checksum:
new_archive_filename = archive_name_from_filename(new_filename)
old_archive_filename = instance.archive_filename
old_archive_path = instance.archive_path
new_archive_path = os.path.join(settings.ARCHIVE_DIR,
new_archive_filename)
if not validate_move(instance, old_archive_path, new_archive_path):
if instance.has_archive_version:
instance.archive_filename = generate_unique_filename(
instance, archive_filename=True
)
move_archive = old_archive_filename != instance.archive_filename # NOQA: E501
else:
move_archive = False
if not move_original and not move_archive:
# Don't do anything if filenames did not change.
return
create_source_path_directory(new_archive_path)
else:
old_archive_path = None
new_archive_path = None
if move_original:
validate_move(instance, old_source_path, instance.source_path)
create_source_path_directory(instance.source_path)
os.rename(old_source_path, instance.source_path)
create_source_path_directory(new_source_path)
try:
os.rename(old_source_path, new_source_path)
if instance.archive_checksum:
os.rename(old_archive_path, new_archive_path)
instance.filename = new_filename
if move_archive:
validate_move(
instance, old_archive_path, instance.archive_path)
create_source_path_directory(instance.archive_path)
os.rename(old_archive_path, instance.archive_path)
# Don't save() here to prevent infinite recursion.
Document.objects.filter(pk=instance.pk).update(
filename=new_filename)
filename=instance.filename,
archive_filename=instance.archive_filename,
)
except OSError as e:
instance.filename = old_filename
# this happens when we can't move a file. If that's the case for
# the archive file, we try our best to revert the changes.
# no need to save the instance, the update() has not happened yet.
except (OSError, DatabaseError, CannotMoveFilesException):
# This happens when either:
# - moving the files failed due to file system errors
# - saving to the database failed due to database errors
# In both cases, we need to revert to the original state.
# Try to move files to their original location.
try:
os.rename(new_source_path, old_source_path)
os.rename(new_archive_path, old_archive_path)
if move_original and os.path.isfile(instance.source_path):
os.rename(instance.source_path, old_source_path)
if move_archive and os.path.isfile(instance.archive_path):
os.rename(instance.archive_path, old_archive_path)
except Exception as e:
# This is fine, since:
# A: if we managed to move source from A to B, we will also
@@ -267,16 +351,10 @@ def update_filename_and_move_files(sender, instance, **kwargs):
# B: if moving the orignal file failed, nothing has changed
# anyway.
pass
except DatabaseError as e:
# this happens after moving files, so move them back into place.
# since moving them once succeeded, it's very likely going to
# succeed again.
os.rename(new_source_path, old_source_path)
if instance.archive_checksum:
os.rename(new_archive_path, old_archive_path)
# restore old values on the instance
instance.filename = old_filename
# again, no need to save the instance, since the actual update()
# operation failed.
instance.archive_filename = old_archive_filename
# finally, remove any empty sub folders. This will do nothing if
# something has failed above.
@@ -284,7 +362,7 @@ def update_filename_and_move_files(sender, instance, **kwargs):
delete_empty_directories(os.path.dirname(old_source_path),
root=settings.ORIGINALS_DIR)
if old_archive_path and not os.path.isfile(old_archive_path):
if instance.has_archive_version and not os.path.isfile(old_archive_path): # NOQA: E501
delete_empty_directories(os.path.dirname(old_archive_path),
root=settings.ARCHIVE_DIR)
@@ -305,4 +383,6 @@ def set_log_entry(sender, document=None, logging_group=None, **kwargs):
def add_to_index(sender, document, **kwargs):
from documents import index
index.add_or_update_document(document)

File diff suppressed because one or more lines are too long

View File

@@ -42,3 +42,58 @@ body {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
@media (prefers-color-scheme: dark) {
/*
From theme_dark.scss
$primary-dark-mode: #45973a;
$danger-dark-mode: #b71631;
$bg-dark-mode: #161618;
$bg-dark-mode-accent: #21262d;
$bg-light-dark-mode: #1c1c1f;
$text-color-dark-mode: #abb2bf;
$border-color-dark-mode: #47494f;
*/
body {
background-color: #161618 !important;
color: #abb2bf;
}
svg.logo .text {
fill: #abb2bf!important;
}
.form-control:not(.is-invalid):not(.btn) {
border-color: #47494f;
}
.form-control:not(.btn) {
background-color: #161618;
color: #abb2bf;
}
.form-control:not(.btn)::placeholder {
color: #abb2bf;
}
.form-control:not(.btn):focus {
background-color: #1c1c1f !important;
color: #8e97a9 !important;
}
.btn-primary {
color: #fff;
background-color: #17541f;
border-color: #17541f;
}
.btn-primary:hover, .btn-primary:focus {
background-color: #0f3614;
border-color: #0c2c10;
}
.btn-primary:not(:disabled):not(.disabled):active {
background-color: #0c2c10;
border-color: #09220d;
}
}

View File

@@ -6,11 +6,12 @@ from django.db.models.signals import post_save
from whoosh.writing import AsyncWriter
from documents import index, sanity_checker
from documents.classifier import DocumentClassifier, \
IncompatibleClassifierVersionError
from documents.classifier import DocumentClassifier, load_classifier
from documents.consumer import Consumer, ConsumerError
from documents.models import Document
from documents.sanity_checker import SanityFailedError
from documents.models import Document, Tag, DocumentType, Correspondent
from documents.sanity_checker import SanityCheckFailedException
logger = logging.getLogger("paperless.tasks")
def index_optimize():
@@ -19,40 +20,45 @@ def index_optimize():
writer.commit(optimize=True)
def index_reindex():
def index_reindex(progress_bar_disable=False):
documents = Document.objects.all()
ix = index.open_index(recreate=True)
with AsyncWriter(ix) as writer:
for document in tqdm.tqdm(documents):
for document in tqdm.tqdm(documents, disable=progress_bar_disable):
index.update_document(writer, document)
def train_classifier():
classifier = DocumentClassifier()
if (not Tag.objects.filter(
matching_algorithm=Tag.MATCH_AUTO).exists() and
not DocumentType.objects.filter(
matching_algorithm=Tag.MATCH_AUTO).exists() and
not Correspondent.objects.filter(
matching_algorithm=Tag.MATCH_AUTO).exists()):
try:
# load the classifier, since we might not have to train it again.
classifier.reload()
except (OSError, EOFError, IncompatibleClassifierVersionError):
# This is what we're going to fix here.
return
classifier = load_classifier()
if not classifier:
classifier = DocumentClassifier()
try:
if classifier.train():
logging.getLogger(__name__).info(
logger.info(
"Saving updated classifier model to {}...".format(
settings.MODEL_FILE)
)
classifier.save_classifier()
classifier.save()
else:
logging.getLogger(__name__).debug(
logger.debug(
"Training data unchanged."
)
except Exception as e:
logging.getLogger(__name__).error(
logger.warning(
"Classifier error: " + str(e)
)
@@ -62,7 +68,8 @@ def consume_file(path,
override_title=None,
override_correspondent_id=None,
override_document_type_id=None,
override_tag_ids=None):
override_tag_ids=None,
task_id=None):
document = Consumer().try_consume_file(
path,
@@ -70,7 +77,9 @@ def consume_file(path,
override_title=override_title,
override_correspondent_id=override_correspondent_id,
override_document_type_id=override_document_type_id,
override_tag_ids=override_tag_ids)
override_tag_ids=override_tag_ids,
task_id=task_id
)
if document:
return "Success. New document id {} created".format(
@@ -84,8 +93,15 @@ def consume_file(path,
def sanity_check():
messages = sanity_checker.check_sanity()
if len(messages) > 0:
raise SanityFailedError(messages)
messages.log_messages()
if messages.has_error():
raise SanityCheckFailedException(
"Sanity check failed with errors. See log.")
elif messages.has_warning():
return "Sanity check exited with warnings. See log."
elif len(messages) > 0:
return "Sanity check exited with infos. See log."
else:
return "No issues detected."

View File

@@ -7,14 +7,16 @@
<head>
<meta charset="utf-8">
<title>Paperless-ng</title>
<base href="/">
<base href="{% url 'base' %}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="username" content="{{username}}">
<meta name="full_name" content="{{full_name}}">
<meta name="cookie_prefix" content="{{cookie_prefix}}">
<meta name="robots" content="noindex,nofollow">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="manifest" href="{% static webmanifest %}">
<link rel="stylesheet" href="{% static styles_css %}">
<link rel="apple-touch-icon" href="{% static apple_touch_icon %}">
</head>
<body>
<app-root>{% translate "Paperless-ng is loading..." %}</app-root>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -0,0 +1 @@
This is a test file.

View File

@@ -4,6 +4,7 @@ from django.contrib.admin.sites import AdminSite
from django.test import TestCase
from django.utils import timezone
from documents import index
from documents.admin import DocumentAdmin
from documents.models import Document
from documents.tests.utils import DirectoriesMixin
@@ -11,49 +12,52 @@ from documents.tests.utils import DirectoriesMixin
class TestDocumentAdmin(DirectoriesMixin, TestCase):
def get_document_from_index(self, doc):
ix = index.open_index()
with ix.searcher() as searcher:
return searcher.document(id=doc.id)
def setUp(self) -> None:
super(TestDocumentAdmin, self).setUp()
self.doc_admin = DocumentAdmin(model=Document, admin_site=AdminSite())
@mock.patch("documents.admin.index.add_or_update_document")
def test_save_model(self, m):
def test_save_model(self):
doc = Document.objects.create(title="test")
doc.title = "new title"
self.doc_admin.save_model(None, doc, None, None)
self.assertEqual(Document.objects.get(id=doc.id).title, "new title")
m.assert_called_once()
self.assertEqual(self.get_document_from_index(doc)['id'], doc.id)
def test_tags(self):
def test_delete_model(self):
doc = Document.objects.create(title="test")
doc.tags.create(name="t1")
doc.tags.create(name="t2")
index.add_or_update_document(doc)
self.assertIsNotNone(self.get_document_from_index(doc))
self.assertEqual(self.doc_admin.tags_(doc), "<span >t1, </span><span >t2, </span>")
def test_tags_empty(self):
doc = Document.objects.create(title="test")
self.assertEqual(self.doc_admin.tags_(doc), "")
@mock.patch("documents.admin.index.remove_document")
def test_delete_model(self, m):
doc = Document.objects.create(title="test")
self.doc_admin.delete_model(None, doc)
self.assertRaises(Document.DoesNotExist, Document.objects.get, id=doc.id)
m.assert_called_once()
@mock.patch("documents.admin.index.remove_document")
def test_delete_queryset(self, m):
self.assertRaises(Document.DoesNotExist, Document.objects.get, id=doc.id)
self.assertIsNone(self.get_document_from_index(doc))
def test_delete_queryset(self):
docs = []
for i in range(42):
Document.objects.create(title="Many documents with the same title", checksum=f"{i:02}")
doc = Document.objects.create(title="Many documents with the same title", checksum=f"{i:02}")
docs.append(doc)
index.add_or_update_document(doc)
self.assertEqual(Document.objects.count(), 42)
for doc in docs:
self.assertIsNotNone(self.get_document_from_index(doc))
self.doc_admin.delete_queryset(None, Document.objects.all())
self.assertEqual(m.call_count, 42)
self.assertEqual(Document.objects.count(), 0)
for doc in docs:
self.assertIsNone(self.get_document_from_index(doc))
def test_created(self):
doc = Document.objects.create(title="test", created=timezone.datetime(2020, 4, 12))
self.assertEqual(self.doc_admin.created_(doc), "2020-04-12")

View File

@@ -1,15 +1,21 @@
import datetime
import io
import json
import os
import shutil
import tempfile
import zipfile
from unittest import mock
import pytest
from django.conf import settings
from django.contrib.auth.models import User
from django.test import override_settings
from rest_framework.test import APITestCase
from whoosh.writing import AsyncWriter
from documents import index, bulk_edit
from documents.models import Document, Correspondent, DocumentType, Tag, SavedView
from documents.models import Document, Correspondent, DocumentType, Tag, SavedView, MatchingModel
from documents.tests.utils import DirectoriesMixin
@@ -144,21 +150,19 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, content_thumbnail)
@override_settings(PAPERLESS_FILENAME_FORMAT="")
def test_download_with_archive(self):
_, filename = tempfile.mkstemp(dir=self.dirs.originals_dir)
content = b"This is a test"
content_archive = b"This is the same test but archived"
with open(filename, "wb") as f:
f.write(content)
filename = os.path.basename(filename)
doc = Document.objects.create(title="none", filename=filename,
doc = Document.objects.create(title="none", filename="my_document.pdf",
archive_filename="archived.pdf",
mime_type="application/pdf")
with open(doc.source_path, "wb") as f:
f.write(content)
with open(doc.archive_path, "wb") as f:
f.write(content_archive)
@@ -228,6 +232,12 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
self.assertEqual(len(results), 2)
self.assertCountEqual([results[0]['id'], results[1]['id']], [doc1.id, doc3.id])
response = self.client.get("/api/documents/?tags__id__in={},{}".format(tag_2.id, tag_3.id))
self.assertEqual(response.status_code, 200)
results = response.data['results']
self.assertEqual(len(results), 2)
self.assertCountEqual([results[0]['id'], results[1]['id']], [doc2.id, doc3.id])
response = self.client.get("/api/documents/?tags__id__all={},{}".format(tag_2.id, tag_3.id))
self.assertEqual(response.status_code, 200)
results = response.data['results']
@@ -261,10 +271,28 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
results = response.data['results']
self.assertEqual(len(results), 0)
def test_search_no_query(self):
response = self.client.get("/api/search/")
results = response.data['results']
def test_documents_title_content_filter(self):
doc1 = Document.objects.create(title="title A", content="content A", checksum="A", mime_type="application/pdf")
doc2 = Document.objects.create(title="title B", content="content A", checksum="B", mime_type="application/pdf")
doc3 = Document.objects.create(title="title A", content="content B", checksum="C", mime_type="application/pdf")
doc4 = Document.objects.create(title="title B", content="content B", checksum="D", mime_type="application/pdf")
response = self.client.get("/api/documents/?title_content=A")
self.assertEqual(response.status_code, 200)
results = response.data['results']
self.assertEqual(len(results), 3)
self.assertCountEqual([results[0]['id'], results[1]['id'], results[2]['id']], [doc1.id, doc2.id, doc3.id])
response = self.client.get("/api/documents/?title_content=B")
self.assertEqual(response.status_code, 200)
results = response.data['results']
self.assertEqual(len(results), 3)
self.assertCountEqual([results[0]['id'], results[1]['id'], results[2]['id']], [doc2.id, doc3.id, doc4.id])
response = self.client.get("/api/documents/?title_content=X")
self.assertEqual(response.status_code, 200)
results = response.data['results']
self.assertEqual(len(results), 0)
def test_search(self):
@@ -278,32 +306,24 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
index.update_document(writer, d1)
index.update_document(writer, d2)
index.update_document(writer, d3)
response = self.client.get("/api/search/?query=bank")
response = self.client.get("/api/documents/?query=bank")
results = response.data['results']
self.assertEqual(response.data['count'], 3)
self.assertEqual(response.data['page'], 1)
self.assertEqual(response.data['page_count'], 1)
self.assertEqual(len(results), 3)
response = self.client.get("/api/search/?query=september")
response = self.client.get("/api/documents/?query=september")
results = response.data['results']
self.assertEqual(response.data['count'], 1)
self.assertEqual(response.data['page'], 1)
self.assertEqual(response.data['page_count'], 1)
self.assertEqual(len(results), 1)
response = self.client.get("/api/search/?query=statement")
response = self.client.get("/api/documents/?query=statement")
results = response.data['results']
self.assertEqual(response.data['count'], 2)
self.assertEqual(response.data['page'], 1)
self.assertEqual(response.data['page_count'], 1)
self.assertEqual(len(results), 2)
response = self.client.get("/api/search/?query=sfegdfg")
response = self.client.get("/api/documents/?query=sfegdfg")
results = response.data['results']
self.assertEqual(response.data['count'], 0)
self.assertEqual(response.data['page'], 0)
self.assertEqual(response.data['page_count'], 0)
self.assertEqual(len(results), 0)
def test_search_multi_page(self):
@@ -316,53 +336,34 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
seen_ids = []
for i in range(1, 6):
response = self.client.get(f"/api/search/?query=content&page={i}")
response = self.client.get(f"/api/documents/?query=content&page={i}&page_size=10")
results = response.data['results']
self.assertEqual(response.data['count'], 55)
self.assertEqual(response.data['page'], i)
self.assertEqual(response.data['page_count'], 6)
self.assertEqual(len(results), 10)
for result in results:
self.assertNotIn(result['id'], seen_ids)
seen_ids.append(result['id'])
response = self.client.get(f"/api/search/?query=content&page=6")
response = self.client.get(f"/api/documents/?query=content&page=6&page_size=10")
results = response.data['results']
self.assertEqual(response.data['count'], 55)
self.assertEqual(response.data['page'], 6)
self.assertEqual(response.data['page_count'], 6)
self.assertEqual(len(results), 5)
for result in results:
self.assertNotIn(result['id'], seen_ids)
seen_ids.append(result['id'])
response = self.client.get(f"/api/search/?query=content&page=7")
results = response.data['results']
self.assertEqual(response.data['count'], 55)
self.assertEqual(response.data['page'], 6)
self.assertEqual(response.data['page_count'], 6)
self.assertEqual(len(results), 5)
def test_search_invalid_page(self):
with AsyncWriter(index.open_index()) as writer:
for i in range(15):
doc = Document.objects.create(checksum=str(i), pk=i+1, title=f"Document {i+1}", content="content")
index.update_document(writer, doc)
first_page = self.client.get(f"/api/search/?query=content&page=1").data
second_page = self.client.get(f"/api/search/?query=content&page=2").data
should_be_first_page_1 = self.client.get(f"/api/search/?query=content&page=0").data
should_be_first_page_2 = self.client.get(f"/api/search/?query=content&page=dgfd").data
should_be_first_page_3 = self.client.get(f"/api/search/?query=content&page=").data
should_be_first_page_4 = self.client.get(f"/api/search/?query=content&page=-7868").data
self.assertDictEqual(first_page, should_be_first_page_1)
self.assertDictEqual(first_page, should_be_first_page_2)
self.assertDictEqual(first_page, should_be_first_page_3)
self.assertDictEqual(first_page, should_be_first_page_4)
self.assertNotEqual(len(first_page['results']), len(second_page['results']))
response = self.client.get(f"/api/documents/?query=content&page=0&page_size=10")
self.assertEqual(response.status_code, 404)
response = self.client.get(f"/api/documents/?query=content&page=3&page_size=10")
self.assertEqual(response.status_code, 404)
@mock.patch("documents.index.autocomplete")
def test_search_autocomplete(self, m):
@@ -386,6 +387,7 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 10)
@pytest.mark.skip(reason="Not implemented yet")
def test_search_spelling_correction(self):
with AsyncWriter(index.open_index()) as writer:
for i in range(55):
@@ -411,7 +413,7 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
index.update_document(writer, d2)
index.update_document(writer, d3)
response = self.client.get(f"/api/search/?more_like={d2.id}")
response = self.client.get(f"/api/documents/?more_like_id={d2.id}")
self.assertEqual(response.status_code, 200)
@@ -421,6 +423,79 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
self.assertEqual(results[0]['id'], d3.id)
self.assertEqual(results[1]['id'], d1.id)
def test_search_filtering(self):
t = Tag.objects.create(name="tag")
t2 = Tag.objects.create(name="tag2")
c = Correspondent.objects.create(name="correspondent")
dt = DocumentType.objects.create(name="type")
d1 = Document.objects.create(checksum="1", correspondent=c, content="test")
d2 = Document.objects.create(checksum="2", document_type=dt, content="test")
d3 = Document.objects.create(checksum="3", content="test")
d3.tags.add(t)
d3.tags.add(t2)
d4 = Document.objects.create(checksum="4", created=datetime.datetime(2020, 7, 13), content="test")
d4.tags.add(t2)
d5 = Document.objects.create(checksum="5", added=datetime.datetime(2020, 7, 13), content="test")
d6 = Document.objects.create(checksum="6", content="test2")
with AsyncWriter(index.open_index()) as writer:
for doc in Document.objects.all():
index.update_document(writer, doc)
def search_query(q):
r = self.client.get("/api/documents/?query=test" + q)
self.assertEqual(r.status_code, 200)
return [hit['id'] for hit in r.data['results']]
self.assertCountEqual(search_query(""), [d1.id, d2.id, d3.id, d4.id, d5.id])
self.assertCountEqual(search_query("&is_tagged=true"), [d3.id, d4.id])
self.assertCountEqual(search_query("&is_tagged=false"), [d1.id, d2.id, d5.id])
self.assertCountEqual(search_query("&correspondent__id=" + str(c.id)), [d1.id])
self.assertCountEqual(search_query("&document_type__id=" + str(dt.id)), [d2.id])
self.assertCountEqual(search_query("&correspondent__isnull"), [d2.id, d3.id, d4.id, d5.id])
self.assertCountEqual(search_query("&document_type__isnull"), [d1.id, d3.id, d4.id, d5.id])
self.assertCountEqual(search_query("&tags__id__all=" + str(t.id) + "," + str(t2.id)), [d3.id])
self.assertCountEqual(search_query("&tags__id__all=" + str(t.id)), [d3.id])
self.assertCountEqual(search_query("&tags__id__all=" + str(t2.id)), [d3.id, d4.id])
self.assertIn(d4.id, search_query("&created__date__lt=" + datetime.datetime(2020, 9, 2).strftime("%Y-%m-%d")))
self.assertNotIn(d4.id, search_query("&created__date__gt=" + datetime.datetime(2020, 9, 2).strftime("%Y-%m-%d")))
self.assertNotIn(d4.id, search_query("&created__date__lt=" + datetime.datetime(2020, 1, 2).strftime("%Y-%m-%d")))
self.assertIn(d4.id, search_query("&created__date__gt=" + datetime.datetime(2020, 1, 2).strftime("%Y-%m-%d")))
self.assertIn(d5.id, search_query("&added__date__lt=" + datetime.datetime(2020, 9, 2).strftime("%Y-%m-%d")))
self.assertNotIn(d5.id, search_query("&added__date__gt=" + datetime.datetime(2020, 9, 2).strftime("%Y-%m-%d")))
self.assertNotIn(d5.id, search_query("&added__date__lt=" + datetime.datetime(2020, 1, 2).strftime("%Y-%m-%d")))
self.assertIn(d5.id, search_query("&added__date__gt=" + datetime.datetime(2020, 1, 2).strftime("%Y-%m-%d")))
def test_search_sorting(self):
c1 = Correspondent.objects.create(name="corres Ax")
c2 = Correspondent.objects.create(name="corres Cx")
c3 = Correspondent.objects.create(name="corres Bx")
d1 = Document.objects.create(checksum="1", correspondent=c1, content="test", archive_serial_number=2, title="3")
d2 = Document.objects.create(checksum="2", correspondent=c2, content="test", archive_serial_number=3, title="2")
d3 = Document.objects.create(checksum="3", correspondent=c3, content="test", archive_serial_number=1, title="1")
with AsyncWriter(index.open_index()) as writer:
for doc in Document.objects.all():
index.update_document(writer, doc)
def search_query(q):
r = self.client.get("/api/documents/?query=test" + q)
self.assertEqual(r.status_code, 200)
return [hit['id'] for hit in r.data['results']]
self.assertListEqual(search_query("&ordering=archive_serial_number"), [d3.id, d1.id, d2.id])
self.assertListEqual(search_query("&ordering=-archive_serial_number"), [d2.id, d1.id, d3.id])
self.assertListEqual(search_query("&ordering=title"), [d3.id, d2.id, d1.id])
self.assertListEqual(search_query("&ordering=-title"), [d1.id, d2.id, d3.id])
self.assertListEqual(search_query("&ordering=correspondent__name"), [d1.id, d3.id, d2.id])
self.assertListEqual(search_query("&ordering=-correspondent__name"), [d2.id, d3.id, d1.id])
def test_statistics(self):
doc1 = Document.objects.create(title="none1", checksum="A")
@@ -436,6 +511,13 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
self.assertEqual(response.data['documents_total'], 3)
self.assertEqual(response.data['documents_inbox'], 1)
def test_statistics_no_inbox_tag(self):
Document.objects.create(title="none1", checksum="A")
response = self.client.get("/api/statistics/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['documents_inbox'], None)
@mock.patch("documents.views.async_task")
def test_upload(self, m):
@@ -569,10 +651,13 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
async_task.assert_not_called()
def test_get_metadata(self):
doc = Document.objects.create(title="test", filename="file.pdf", mime_type="image/png", archive_checksum="A")
doc = Document.objects.create(title="test", filename="file.pdf", mime_type="image/png", archive_checksum="A", archive_filename="archive.pdf")
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "thumbnails", "0000001.png"), doc.source_path)
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), doc.archive_path)
source_file = os.path.join(os.path.dirname(__file__), "samples", "documents", "thumbnails", "0000001.png")
archive_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf")
shutil.copy(source_file, doc.source_path)
shutil.copy(archive_file, doc.archive_path)
response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
self.assertEqual(response.status_code, 200)
@@ -583,6 +668,14 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
self.assertTrue(meta['has_archive_version'])
self.assertEqual(len(meta['original_metadata']), 0)
self.assertGreater(len(meta['archive_metadata']), 0)
self.assertEqual(meta['media_filename'], "file.pdf")
self.assertEqual(meta['archive_media_filename'], "archive.pdf")
self.assertEqual(meta['original_size'], os.stat(source_file).st_size)
self.assertEqual(meta['archive_size'], os.stat(archive_file).st_size)
def test_get_metadata_invalid_doc(self):
response = self.client.get(f"/api/documents/34576/metadata/")
self.assertEqual(response.status_code, 404)
def test_get_metadata_no_archive(self):
doc = Document.objects.create(title="test", filename="file.pdf", mime_type="application/pdf")
@@ -598,6 +691,46 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
self.assertFalse(meta['has_archive_version'])
self.assertGreater(len(meta['original_metadata']), 0)
self.assertIsNone(meta['archive_metadata'])
self.assertIsNone(meta['archive_media_filename'])
def test_get_metadata_missing_files(self):
doc = Document.objects.create(title="test", filename="file.pdf", mime_type="application/pdf", archive_filename="file.pdf", archive_checksum="B", checksum="A")
response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
self.assertEqual(response.status_code, 200)
meta = response.data
self.assertTrue(meta['has_archive_version'])
self.assertIsNone(meta['original_metadata'])
self.assertIsNone(meta['original_size'])
self.assertIsNone(meta['archive_metadata'])
self.assertIsNone(meta['archive_size'])
def test_get_empty_suggestions(self):
doc = Document.objects.create(title="test", mime_type="application/pdf")
response = self.client.get(f"/api/documents/{doc.pk}/suggestions/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {'correspondents': [], 'tags': [], 'document_types': []})
def test_get_suggestions_invalid_doc(self):
response = self.client.get(f"/api/documents/34676/suggestions/")
self.assertEqual(response.status_code, 404)
@mock.patch("documents.views.match_correspondents")
@mock.patch("documents.views.match_tags")
@mock.patch("documents.views.match_document_types")
def test_get_suggestions(self, match_document_types, match_tags, match_correspondents):
doc = Document.objects.create(title="test", mime_type="application/pdf", content="this is an invoice!")
match_tags.return_value = [Tag(id=56), Tag(id=123)]
match_document_types.return_value = [DocumentType(id=23)]
match_correspondents.return_value = [Correspondent(id=88), Correspondent(id=2)]
response = self.client.get(f"/api/documents/{doc.pk}/suggestions/")
self.assertEqual(response.data, {'correspondents': [88,2], 'tags': [56,123], 'document_types': [23]})
def test_saved_views(self):
u1 = User.objects.create_user("user1")
@@ -683,6 +816,126 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
v1 = SavedView.objects.get(id=v1.id)
self.assertEqual(v1.filter_rules.count(), 0)
def test_get_logs(self):
response = self.client.get("/api/logs/")
self.assertEqual(response.status_code, 200)
self.assertCountEqual(response.data, ["mail", "paperless"])
def test_get_invalid_log(self):
response = self.client.get("/api/logs/bogus_log/")
self.assertEqual(response.status_code, 404)
@override_settings(LOGGING_DIR="bogus_dir")
def test_get_nonexistent_log(self):
response = self.client.get("/api/logs/paperless/")
self.assertEqual(response.status_code, 404)
def test_get_log(self):
log_data = "test\ntest2\n"
with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f:
f.write(log_data)
response = self.client.get("/api/logs/paperless/")
self.assertEqual(response.status_code, 200)
self.assertListEqual(response.data, ["test", "test2"])
def test_invalid_regex_other_algorithm(self):
for endpoint in ['correspondents', 'tags', 'document_types']:
response = self.client.post(f"/api/{endpoint}/", {
"name": "test",
"matching_algorithm": MatchingModel.MATCH_ANY,
"match": "["
}, format='json')
self.assertEqual(response.status_code, 201, endpoint)
def test_invalid_regex(self):
for endpoint in ['correspondents', 'tags', 'document_types']:
response = self.client.post(f"/api/{endpoint}/", {
"name": "test",
"matching_algorithm": MatchingModel.MATCH_REGEX,
"match": "["
}, format='json')
self.assertEqual(response.status_code, 400, endpoint)
def test_valid_regex(self):
for endpoint in ['correspondents', 'tags', 'document_types']:
response = self.client.post(f"/api/{endpoint}/", {
"name": "test",
"matching_algorithm": MatchingModel.MATCH_REGEX,
"match": "[0-9]"
}, format='json')
self.assertEqual(response.status_code, 201, endpoint)
def test_regex_no_algorithm(self):
for endpoint in ['correspondents', 'tags', 'document_types']:
response = self.client.post(f"/api/{endpoint}/", {
"name": "test",
"match": "[0-9]"
}, format='json')
self.assertEqual(response.status_code, 201, endpoint)
def test_tag_color_default(self):
response = self.client.post("/api/tags/", {
"name": "tag"
}, format="json")
self.assertEqual(response.status_code, 201)
self.assertEqual(Tag.objects.get(id=response.data['id']).color, "#a6cee3")
self.assertEqual(self.client.get(f"/api/tags/{response.data['id']}/", format="json").data['colour'], 1)
def test_tag_color(self):
response = self.client.post("/api/tags/", {
"name": "tag",
"colour": 3
}, format="json")
self.assertEqual(response.status_code, 201)
self.assertEqual(Tag.objects.get(id=response.data['id']).color, "#b2df8a")
self.assertEqual(self.client.get(f"/api/tags/{response.data['id']}/", format="json").data['colour'], 3)
def test_tag_color_invalid(self):
response = self.client.post("/api/tags/", {
"name": "tag",
"colour": 34
}, format="json")
self.assertEqual(response.status_code, 400)
def test_tag_color_custom(self):
tag = Tag.objects.create(name="test", color="#abcdef")
self.assertEqual(self.client.get(f"/api/tags/{tag.id}/", format="json").data['colour'], 1)
class TestDocumentApiV2(DirectoriesMixin, APITestCase):
def setUp(self):
super(TestDocumentApiV2, self).setUp()
self.user = User.objects.create_superuser(username="temp_admin")
self.client.force_login(user=self.user)
self.client.defaults['HTTP_ACCEPT'] = 'application/json; version=2'
def test_tag_validate_color(self):
self.assertEqual(self.client.post("/api/tags/", {"name": "test", "color": "#12fFaA"}, format="json").status_code, 201)
self.assertEqual(self.client.post("/api/tags/", {"name": "test1", "color": "abcdef"}, format="json").status_code, 400)
self.assertEqual(self.client.post("/api/tags/", {"name": "test2", "color": "#abcdfg"}, format="json").status_code, 400)
self.assertEqual(self.client.post("/api/tags/", {"name": "test3", "color": "#asd"}, format="json").status_code, 400)
self.assertEqual(self.client.post("/api/tags/", {"name": "test4", "color": "#12121212"}, format="json").status_code, 400)
def test_tag_text_color(self):
t = Tag.objects.create(name="tag1", color="#000000")
self.assertEqual(self.client.get(f"/api/tags/{t.id}/", format="json").data['text_color'], "#ffffff")
t.color = "#ffffff"
t.save()
self.assertEqual(self.client.get(f"/api/tags/{t.id}/", format="json").data['text_color'], "#000000")
t.color = "asdf"
t.save()
self.assertEqual(self.client.get(f"/api/tags/{t.id}/", format="json").data['text_color'], "#000000")
t.color = "123"
t.save()
self.assertEqual(self.client.get(f"/api/tags/{t.id}/", format="json").data['text_color'], "#000000")
class TestBulkEdit(DirectoriesMixin, APITestCase):
@@ -1037,6 +1290,113 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
self.assertCountEqual(response.data['selected_document_types'], [{"id": self.c1.id, "document_count": 1}, {"id": self.c2.id, "document_count": 0}])
class TestBulkDownload(DirectoriesMixin, APITestCase):
def setUp(self):
super(TestBulkDownload, self).setUp()
user = User.objects.create_superuser(username="temp_admin")
self.client.force_login(user=user)
self.doc1 = Document.objects.create(title="unrelated", checksum="A")
self.doc2 = Document.objects.create(title="document A", filename="docA.pdf", mime_type="application/pdf", checksum="B", created=datetime.datetime(2021, 1, 1))
self.doc2b = Document.objects.create(title="document A", filename="docA2.pdf", mime_type="application/pdf", checksum="D", created=datetime.datetime(2021, 1, 1))
self.doc3 = Document.objects.create(title="document B", filename="docB.jpg", mime_type="image/jpeg", checksum="C", created=datetime.datetime(2020, 3, 21), archive_filename="docB.pdf", archive_checksum="D")
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), self.doc2.source_path)
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "simple.png"), self.doc2b.source_path)
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "simple.jpg"), self.doc3.source_path)
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "test_with_bom.pdf"), self.doc3.archive_path)
def test_download_originals(self):
response = self.client.post("/api/documents/bulk_download/", json.dumps({
"documents": [self.doc2.id, self.doc3.id],
"content": "originals"
}), content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/zip')
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
self.assertEqual(len(zipf.filelist), 2)
self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
self.assertIn("2020-03-21 document B.jpg", zipf.namelist())
with self.doc2.source_file as f:
self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
with self.doc3.source_file as f:
self.assertEqual(f.read(), zipf.read("2020-03-21 document B.jpg"))
def test_download_default(self):
response = self.client.post("/api/documents/bulk_download/", json.dumps({
"documents": [self.doc2.id, self.doc3.id]
}), content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/zip')
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
self.assertEqual(len(zipf.filelist), 2)
self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
self.assertIn("2020-03-21 document B.pdf", zipf.namelist())
with self.doc2.source_file as f:
self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
with self.doc3.archive_file as f:
self.assertEqual(f.read(), zipf.read("2020-03-21 document B.pdf"))
def test_download_both(self):
response = self.client.post("/api/documents/bulk_download/", json.dumps({
"documents": [self.doc2.id, self.doc3.id],
"content": "both"
}), content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/zip')
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
self.assertEqual(len(zipf.filelist), 3)
self.assertIn("originals/2021-01-01 document A.pdf", zipf.namelist())
self.assertIn("archive/2020-03-21 document B.pdf", zipf.namelist())
self.assertIn("originals/2020-03-21 document B.jpg", zipf.namelist())
with self.doc2.source_file as f:
self.assertEqual(f.read(), zipf.read("originals/2021-01-01 document A.pdf"))
with self.doc3.archive_file as f:
self.assertEqual(f.read(), zipf.read("archive/2020-03-21 document B.pdf"))
with self.doc3.source_file as f:
self.assertEqual(f.read(), zipf.read("originals/2020-03-21 document B.jpg"))
def test_filename_clashes(self):
response = self.client.post("/api/documents/bulk_download/", json.dumps({
"documents": [self.doc2.id, self.doc2b.id]
}), content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/zip')
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
self.assertEqual(len(zipf.filelist), 2)
self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
self.assertIn("2021-01-01 document A_01.pdf", zipf.namelist())
with self.doc2.source_file as f:
self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
with self.doc2b.source_file as f:
self.assertEqual(f.read(), zipf.read("2021-01-01 document A_01.pdf"))
def test_compression(self):
response = self.client.post("/api/documents/bulk_download/", json.dumps({
"documents": [self.doc2.id, self.doc2b.id],
"compression": "lzma"
}), content_type='application/json')
class TestApiAuth(APITestCase):
def test_auth_required(self):
@@ -1057,7 +1417,20 @@ class TestApiAuth(APITestCase):
self.assertEqual(self.client.get("/api/logs/").status_code, 401)
self.assertEqual(self.client.get("/api/saved_views/").status_code, 401)
self.assertEqual(self.client.get("/api/search/").status_code, 401)
self.assertEqual(self.client.get("/api/search/auto_complete/").status_code, 401)
self.assertEqual(self.client.get("/api/search/autocomplete/").status_code, 401)
self.assertEqual(self.client.get("/api/documents/bulk_edit/").status_code, 401)
self.assertEqual(self.client.get("/api/documents/bulk_download/").status_code, 401)
self.assertEqual(self.client.get("/api/documents/selection_data/").status_code, 401)
def test_api_version_no_auth(self):
response = self.client.get("/api/")
self.assertNotIn("X-Api-Version", response)
self.assertNotIn("X-Version", response)
def test_api_version_with_auth(self):
user = User.objects.create_superuser(username="test")
self.client.force_login(user)
response = self.client.get("/api/")
self.assertIn("X-Api-Version", response)
self.assertIn("X-Version", response)

View File

@@ -1,10 +1,13 @@
import os
import tempfile
from time import sleep
from pathlib import Path
from unittest import mock
import pytest
from django.conf import settings
from django.test import TestCase, override_settings
from documents.classifier import DocumentClassifier, IncompatibleClassifierVersionError
from documents.classifier import DocumentClassifier, IncompatibleClassifierVersionError, load_classifier
from documents.models import Correspondent, Document, Tag, DocumentType
from documents.tests.utils import DirectoriesMixin
@@ -82,37 +85,19 @@ class TestClassifier(DirectoriesMixin, TestCase):
self.assertTrue(self.classifier.train())
self.assertFalse(self.classifier.train())
self.classifier.save_classifier()
self.classifier.save()
classifier2 = DocumentClassifier()
current_ver = DocumentClassifier.FORMAT_VERSION
with mock.patch("documents.classifier.DocumentClassifier.FORMAT_VERSION", current_ver+1):
# assure that we won't load old classifiers.
self.assertRaises(IncompatibleClassifierVersionError, classifier2.reload)
self.assertRaises(IncompatibleClassifierVersionError, classifier2.load)
self.classifier.save_classifier()
self.classifier.save()
# assure that we can load the classifier after saving it.
classifier2.reload()
def testReload(self):
self.generate_test_data()
self.assertTrue(self.classifier.train())
self.classifier.save_classifier()
classifier2 = DocumentClassifier()
classifier2.reload()
v1 = classifier2.classifier_version
# change the classifier after some time.
sleep(1)
self.classifier.save_classifier()
classifier2.reload()
v2 = classifier2.classifier_version
self.assertNotEqual(v1, v2)
classifier2.load()
@override_settings(DATA_DIR=tempfile.mkdtemp())
def testSaveClassifier(self):
@@ -121,12 +106,21 @@ class TestClassifier(DirectoriesMixin, TestCase):
self.classifier.train()
self.classifier.save_classifier()
self.classifier.save()
new_classifier = DocumentClassifier()
new_classifier.reload()
new_classifier.load()
self.assertFalse(new_classifier.train())
@override_settings(MODEL_FILE=os.path.join(os.path.dirname(__file__), "data", "model.pickle"))
def test_load_and_classify(self):
self.generate_test_data()
new_classifier = DocumentClassifier()
new_classifier.load()
self.assertCountEqual(new_classifier.predict_tags(self.doc2.content), [45, 12])
def test_one_correspondent_predict(self):
c1 = Correspondent.objects.create(name="c1", matching_algorithm=Correspondent.MATCH_AUTO)
doc1 = Document.objects.create(title="doc1", content="this is a document from c1", correspondent=c1, checksum="A")
@@ -235,3 +229,42 @@ class TestClassifier(DirectoriesMixin, TestCase):
self.classifier.train()
self.assertListEqual(self.classifier.predict_tags(doc1.content), [t1.pk])
self.assertListEqual(self.classifier.predict_tags(doc2.content), [])
def test_load_classifier_not_exists(self):
self.assertFalse(os.path.exists(settings.MODEL_FILE))
self.assertIsNone(load_classifier())
@mock.patch("documents.classifier.DocumentClassifier.load")
def test_load_classifier(self, load):
Path(settings.MODEL_FILE).touch()
self.assertIsNotNone(load_classifier())
load.assert_called_once()
@override_settings(CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}})
@override_settings(MODEL_FILE=os.path.join(os.path.dirname(__file__), "data", "model.pickle"))
@pytest.mark.skip(reason="Disabled caching due to high memory usage - need to investigate.")
def test_load_classifier_cached(self):
classifier = load_classifier()
self.assertIsNotNone(classifier)
with mock.patch("documents.classifier.DocumentClassifier.load") as load:
classifier2 = load_classifier()
load.assert_not_called()
@mock.patch("documents.classifier.DocumentClassifier.load")
def test_load_classifier_incompatible_version(self, load):
Path(settings.MODEL_FILE).touch()
self.assertTrue(os.path.exists(settings.MODEL_FILE))
load.side_effect = IncompatibleClassifierVersionError()
self.assertIsNone(load_classifier())
self.assertFalse(os.path.exists(settings.MODEL_FILE))
@mock.patch("documents.classifier.DocumentClassifier.load")
def test_load_classifier_os_error(self, load):
Path(settings.MODEL_FILE).touch()
self.assertTrue(os.path.exists(settings.MODEL_FILE))
load.side_effect = OSError()
self.assertIsNone(load_classifier())
self.assertTrue(os.path.exists(settings.MODEL_FILE))

View File

@@ -5,12 +5,14 @@ import tempfile
from unittest import mock
from unittest.mock import MagicMock
from django.conf import settings
from django.test import TestCase, override_settings
from .utils import DirectoriesMixin
from ..consumer import Consumer, ConsumerError
from ..models import FileInfo, Tag, Correspondent, DocumentType, Document
from ..parsers import DocumentParser, ParseError
from ..tasks import sanity_check
class TestAttributes(TestCase):
@@ -165,25 +167,43 @@ class TestFieldPermutations(TestCase):
class DummyParser(DocumentParser):
def get_thumbnail(self, document_path, mime_type):
def get_thumbnail(self, document_path, mime_type, file_name=None):
# not important during tests
raise NotImplementedError()
def __init__(self, logging_group, scratch_dir, archive_path):
super(DummyParser, self).__init__(logging_group)
super(DummyParser, self).__init__(logging_group, None)
_, self.fake_thumb = tempfile.mkstemp(suffix=".png", dir=scratch_dir)
self.archive_path = archive_path
def get_optimised_thumbnail(self, document_path, mime_type):
def get_optimised_thumbnail(self, document_path, mime_type, file_name=None):
return self.fake_thumb
def parse(self, document_path, mime_type, file_name=None):
self.text = "The Text"
class CopyParser(DocumentParser):
def get_thumbnail(self, document_path, mime_type, file_name=None):
return self.fake_thumb
def get_optimised_thumbnail(self, document_path, mime_type, file_name=None):
return self.fake_thumb
def __init__(self, logging_group, progress_callback=None):
super(CopyParser, self).__init__(logging_group, progress_callback)
_, self.fake_thumb = tempfile.mkstemp(suffix=".png", dir=self.tempdir)
def parse(self, document_path, mime_type, file_name=None):
self.text = "The text"
self.archive_path = os.path.join(self.tempdir, "archive.pdf")
shutil.copy(document_path, self.archive_path)
class FaultyParser(DocumentParser):
def get_thumbnail(self, document_path, mime_type):
def get_thumbnail(self, document_path, mime_type, file_name=None):
# not important during tests
raise NotImplementedError()
@@ -191,7 +211,7 @@ class FaultyParser(DocumentParser):
super(FaultyParser, self).__init__(logging_group)
_, self.fake_thumb = tempfile.mkstemp(suffix=".png", dir=scratch_dir)
def get_optimised_thumbnail(self, document_path, mime_type):
def get_optimised_thumbnail(self, document_path, mime_type, file_name=None):
return self.fake_thumb
def parse(self, document_path, mime_type, file_name=None):
@@ -203,6 +223,8 @@ def fake_magic_from_file(file, mime=False):
if mime:
if os.path.splitext(file)[1] == ".pdf":
return "application/pdf"
elif os.path.splitext(file)[1] == ".png":
return "image/png"
else:
return "unknown"
else:
@@ -212,10 +234,24 @@ def fake_magic_from_file(file, mime=False):
@mock.patch("documents.consumer.magic.from_file", fake_magic_from_file)
class TestConsumer(DirectoriesMixin, TestCase):
def make_dummy_parser(self, logging_group):
def _assert_first_last_send_progress(self, first_status="STARTING", last_status="SUCCESS", first_progress=0, first_progress_max=100, last_progress=100, last_progress_max=100):
self._send_progress.assert_called()
args, kwargs = self._send_progress.call_args_list[0]
self.assertEqual(args[0], first_progress)
self.assertEqual(args[1], first_progress_max)
self.assertEqual(args[2], first_status)
args, kwargs = self._send_progress.call_args_list[len(self._send_progress.call_args_list) - 1]
self.assertEqual(args[0], last_progress)
self.assertEqual(args[1], last_progress_max)
self.assertEqual(args[2], last_status)
def make_dummy_parser(self, logging_group, progress_callback=None):
return DummyParser(logging_group, self.dirs.scratch_dir, self.get_test_archive_file())
def make_faulty_parser(self, logging_group):
def make_faulty_parser(self, logging_group, progress_callback=None):
return FaultyParser(logging_group, self.dirs.scratch_dir)
def setUp(self):
@@ -228,7 +264,11 @@ class TestConsumer(DirectoriesMixin, TestCase):
"mime_types": {"application/pdf": ".pdf"},
"weight": 0
})]
self.addCleanup(patcher.stop)
# this prevents websocket message reports during testing.
patcher = mock.patch("documents.consumer.Consumer._send_progress")
self._send_progress = patcher.start()
self.addCleanup(patcher.stop)
self.consumer = Consumer()
@@ -256,6 +296,7 @@ class TestConsumer(DirectoriesMixin, TestCase):
self.assertIsNone(document.correspondent)
self.assertIsNone(document.document_type)
self.assertEqual(document.filename, "0000001.pdf")
self.assertEqual(document.archive_filename, "0000001.pdf")
self.assertTrue(os.path.isfile(
document.source_path
@@ -274,6 +315,29 @@ class TestConsumer(DirectoriesMixin, TestCase):
self.assertFalse(os.path.isfile(filename))
self._assert_first_last_send_progress()
@override_settings(PAPERLESS_FILENAME_FORMAT=None)
def testDeleteMacFiles(self):
# https://github.com/jonaswinkler/paperless-ng/discussions/1037
filename = self.get_test_file()
shadow_file = os.path.join(self.dirs.scratch_dir, "._sample.pdf")
shutil.copy(filename, shadow_file)
self.assertTrue(os.path.isfile(shadow_file))
document = self.consumer.try_consume_file(filename)
self.assertTrue(os.path.isfile(
document.source_path
))
self.assertFalse(os.path.isfile(shadow_file))
self.assertFalse(os.path.isfile(filename))
def testOverrideFilename(self):
filename = self.get_test_file()
override_filename = "Statement for November.pdf"
@@ -282,21 +346,26 @@ class TestConsumer(DirectoriesMixin, TestCase):
self.assertEqual(document.title, "Statement for November")
self._assert_first_last_send_progress()
def testOverrideTitle(self):
document = self.consumer.try_consume_file(self.get_test_file(), override_title="Override Title")
self.assertEqual(document.title, "Override Title")
self._assert_first_last_send_progress()
def testOverrideCorrespondent(self):
c = Correspondent.objects.create(name="test")
document = self.consumer.try_consume_file(self.get_test_file(), override_correspondent_id=c.pk)
self.assertEqual(document.correspondent.id, c.id)
self._assert_first_last_send_progress()
def testOverrideDocumentType(self):
dt = DocumentType.objects.create(name="test")
document = self.consumer.try_consume_file(self.get_test_file(), override_document_type_id=dt.pk)
self.assertEqual(document.document_type.id, dt.id)
self._assert_first_last_send_progress()
def testOverrideTags(self):
t1 = Tag.objects.create(name="t1")
@@ -307,37 +376,42 @@ class TestConsumer(DirectoriesMixin, TestCase):
self.assertIn(t1, document.tags.all())
self.assertNotIn(t2, document.tags.all())
self.assertIn(t3, document.tags.all())
self._assert_first_last_send_progress()
def testNotAFile(self):
try:
self.consumer.try_consume_file("non-existing-file")
except ConsumerError as e:
self.assertTrue(str(e).endswith('It is not a file'))
return
self.fail("Should throw exception")
self.assertRaisesMessage(
ConsumerError,
"File not found",
self.consumer.try_consume_file,
"non-existing-file"
)
self._assert_first_last_send_progress(last_status="FAILED")
def testDuplicates1(self):
self.consumer.try_consume_file(self.get_test_file())
try:
self.consumer.try_consume_file(self.get_test_file())
except ConsumerError as e:
self.assertTrue(str(e).endswith("It is a duplicate."))
return
self.assertRaisesMessage(
ConsumerError,
"It is a duplicate",
self.consumer.try_consume_file,
self.get_test_file()
)
self.fail("Should throw exception")
self._assert_first_last_send_progress(last_status="FAILED")
def testDuplicates2(self):
self.consumer.try_consume_file(self.get_test_file())
try:
self.consumer.try_consume_file(self.get_test_archive_file())
except ConsumerError as e:
self.assertTrue(str(e).endswith("It is a duplicate."))
return
self.assertRaisesMessage(
ConsumerError,
"It is a duplicate",
self.consumer.try_consume_file,
self.get_test_archive_file()
)
self.fail("Should throw exception")
self._assert_first_last_send_progress(last_status="FAILED")
def testDuplicates3(self):
self.consumer.try_consume_file(self.get_test_archive_file())
@@ -347,13 +421,15 @@ class TestConsumer(DirectoriesMixin, TestCase):
def testNoParsers(self, m):
m.return_value = []
try:
self.consumer.try_consume_file(self.get_test_file())
except ConsumerError as e:
self.assertEqual("Unsupported mime type application/pdf of file sample.pdf", str(e))
return
self.assertRaisesMessage(
ConsumerError,
"sample.pdf: Unsupported mime type application/pdf",
self.consumer.try_consume_file,
self.get_test_file()
)
self._assert_first_last_send_progress(last_status="FAILED")
self.fail("Should throw exception")
@mock.patch("documents.parsers.document_consumer_declaration.send")
def testFaultyParser(self, m):
@@ -363,24 +439,28 @@ class TestConsumer(DirectoriesMixin, TestCase):
"weight": 0
})]
try:
self.consumer.try_consume_file(self.get_test_file())
except ConsumerError as e:
self.assertEqual(str(e), "Does not compute.")
return
self.assertRaisesMessage(
ConsumerError,
"sample.pdf: Error while consuming document sample.pdf: Does not compute.",
self.consumer.try_consume_file,
self.get_test_file()
)
self.fail("Should throw exception.")
self._assert_first_last_send_progress(last_status="FAILED")
@mock.patch("documents.consumer.Consumer._write")
def testPostSaveError(self, m):
filename = self.get_test_file()
m.side_effect = OSError("NO.")
try:
self.consumer.try_consume_file(filename)
except ConsumerError as e:
self.assertEqual(str(e), "NO.")
else:
self.fail("Should raise exception")
self.assertRaisesMessage(
ConsumerError,
"sample.pdf: The following error occured while consuming sample.pdf: NO.",
self.consumer.try_consume_file,
filename
)
self._assert_first_last_send_progress(last_status="FAILED")
# file not deleted
self.assertTrue(os.path.isfile(filename))
@@ -396,6 +476,9 @@ class TestConsumer(DirectoriesMixin, TestCase):
self.assertEqual(document.title, "new docs")
self.assertEqual(document.filename, "none/new docs.pdf")
self.assertEqual(document.archive_filename, "none/new docs.pdf")
self._assert_first_last_send_progress()
@override_settings(PAPERLESS_FILENAME_FORMAT="{correspondent}/{title}")
@mock.patch("documents.signals.handlers.generate_unique_filename")
@@ -408,7 +491,7 @@ class TestConsumer(DirectoriesMixin, TestCase):
filenames.insert(0, f)
return f
m.side_effect = lambda f, root: get_filename()
m.side_effect = lambda f, archive_filename = False: get_filename()
filename = self.get_test_file()
@@ -419,8 +502,11 @@ class TestConsumer(DirectoriesMixin, TestCase):
self.assertEqual(document.title, "new docs")
self.assertIsNotNone(os.path.isfile(document.title))
self.assertTrue(os.path.isfile(document.source_path))
self.assertTrue(os.path.isfile(document.archive_path))
@mock.patch("documents.consumer.DocumentClassifier")
self._assert_first_last_send_progress()
@mock.patch("documents.consumer.load_classifier")
def testClassifyDocument(self, m):
correspondent = Correspondent.objects.create(name="test")
dtype = DocumentType.objects.create(name="test")
@@ -439,19 +525,26 @@ class TestConsumer(DirectoriesMixin, TestCase):
self.assertIn(t1, document.tags.all())
self.assertNotIn(t2, document.tags.all())
self._assert_first_last_send_progress()
@override_settings(CONSUMER_DELETE_DUPLICATES=True)
def test_delete_duplicate(self):
dst = self.get_test_file()
self.assertTrue(os.path.isfile(dst))
doc = self.consumer.try_consume_file(dst)
self._assert_first_last_send_progress()
self.assertFalse(os.path.isfile(dst))
self.assertIsNotNone(doc)
self._send_progress.reset_mock()
dst = self.get_test_file()
self.assertTrue(os.path.isfile(dst))
self.assertRaises(ConsumerError, self.consumer.try_consume_file, dst)
self.assertFalse(os.path.isfile(dst))
self._assert_first_last_send_progress(last_status="FAILED")
@override_settings(CONSUMER_DELETE_DUPLICATES=False)
def test_no_delete_duplicate(self):
@@ -467,6 +560,32 @@ class TestConsumer(DirectoriesMixin, TestCase):
self.assertRaises(ConsumerError, self.consumer.try_consume_file, dst)
self.assertTrue(os.path.isfile(dst))
self._assert_first_last_send_progress(last_status="FAILED")
@override_settings(PAPERLESS_FILENAME_FORMAT="{title}")
@mock.patch("documents.parsers.document_consumer_declaration.send")
def test_similar_filenames(self, m):
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), os.path.join(settings.CONSUMPTION_DIR, "simple.pdf"))
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "simple.png"), os.path.join(settings.CONSUMPTION_DIR, "simple.png"))
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "simple-noalpha.png"), os.path.join(settings.CONSUMPTION_DIR, "simple.png.pdf"))
m.return_value = [(None, {
"parser": CopyParser,
"mime_types": {"application/pdf": ".pdf", "image/png": ".png"},
"weight": 0
})]
doc1 = self.consumer.try_consume_file(os.path.join(settings.CONSUMPTION_DIR, "simple.png"))
doc2 = self.consumer.try_consume_file(os.path.join(settings.CONSUMPTION_DIR, "simple.pdf"))
doc3 = self.consumer.try_consume_file(os.path.join(settings.CONSUMPTION_DIR, "simple.png.pdf"))
self.assertEqual(doc1.filename, "simple.png")
self.assertEqual(doc1.archive_filename, "simple.pdf")
self.assertEqual(doc2.filename, "simple.pdf")
self.assertEqual(doc2.archive_filename, "simple_01.pdf")
self.assertEqual(doc3.filename, "simple.png.pdf")
self.assertEqual(doc3.archive_filename, "simple.png.pdf")
sanity_check()
class PreConsumeTestCase(TestCase):
@@ -479,9 +598,11 @@ class PreConsumeTestCase(TestCase):
m.assert_not_called()
@mock.patch("documents.consumer.Popen")
@mock.patch("documents.consumer.Consumer._send_progress")
@override_settings(PRE_CONSUME_SCRIPT="does-not-exist")
def test_pre_consume_script_not_found(self, m):
def test_pre_consume_script_not_found(self, m, m2):
c = Consumer()
c.filename = "somefile.pdf"
c.path = "path-to-file"
self.assertRaises(ConsumerError, c.run_pre_consume_script)
@@ -503,7 +624,6 @@ class PreConsumeTestCase(TestCase):
self.assertEqual(command[1], "path-to-file")
class PostConsumeTestCase(TestCase):
@mock.patch("documents.consumer.Popen")
@@ -519,12 +639,13 @@ class PostConsumeTestCase(TestCase):
m.assert_not_called()
@override_settings(POST_CONSUME_SCRIPT="does-not-exist")
def test_post_consume_script_not_found(self):
@mock.patch("documents.consumer.Consumer._send_progress")
def test_post_consume_script_not_found(self, m):
doc = Document.objects.create(title="Test", mime_type="application/pdf")
self.assertRaises(ConsumerError, Consumer().run_post_consume_script, doc)
c = Consumer()
c.filename = "somefile.pdf"
self.assertRaises(ConsumerError, c.run_post_consume_script, doc)
@mock.patch("documents.consumer.Popen")
def test_post_consume_script_simple(self, m):

View File

@@ -1,7 +1,6 @@
import datetime
import os
import shutil
from unittest import mock
from uuid import uuid4
from dateutil import tz
@@ -9,7 +8,6 @@ from django.conf import settings
from django.test import TestCase, override_settings
from documents.parsers import parse_date
from paperless_tesseract.parsers import RasterisedDocumentParser
class TestDate(TestCase):
@@ -152,4 +150,4 @@ class TestDate(TestCase):
2018, 2, 13, 0, 0,
tzinfo=tz.gettz(settings.TIME_ZONE)
)
)
)

View File

@@ -201,6 +201,13 @@ class TestFileHandling(DirectoriesMixin, TestCase):
self.assertEqual(generate_filename(d), "my_doc_type - the_doc.pdf")
@override_settings(PAPERLESS_FILENAME_FORMAT="{asn} - {title}")
def test_asn(self):
d1 = Document.objects.create(title="the_doc", mime_type="application/pdf", archive_serial_number=652, checksum="A")
d2 = Document.objects.create(title="the_doc", mime_type="application/pdf", archive_serial_number=None, checksum="B")
self.assertEqual(generate_filename(d1), "652 - the_doc.pdf")
self.assertEqual(generate_filename(d2), "none - the_doc.pdf")
@override_settings(PAPERLESS_FILENAME_FORMAT="{tags[type]}")
def test_tags_with_underscore(self):
document = Document()
@@ -439,6 +446,18 @@ class TestFileHandling(DirectoriesMixin, TestCase):
self.assertEqual(document2.filename, "qwe.pdf")
@override_settings(PAPERLESS_FILENAME_FORMAT="{title}")
@mock.patch("documents.signals.handlers.Document.objects.filter")
def test_no_update_without_change(self, m):
doc = Document.objects.create(title="document", filename="document.pdf", archive_filename="document.pdf", checksum="A", archive_checksum="B", mime_type="application/pdf")
Path(doc.source_path).touch()
Path(doc.archive_path).touch()
doc.save()
m.assert_not_called()
class TestFileHandlingWithArchive(DirectoriesMixin, TestCase):
@@ -448,7 +467,7 @@ class TestFileHandlingWithArchive(DirectoriesMixin, TestCase):
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
Path(original).touch()
Path(archive).touch()
doc = Document.objects.create(mime_type="application/pdf", filename="0000001.pdf", checksum="A", archive_checksum="B")
doc = Document.objects.create(mime_type="application/pdf", filename="0000001.pdf", checksum="A", archive_filename="0000001.pdf", archive_checksum="B")
self.assertTrue(os.path.isfile(original))
self.assertTrue(os.path.isfile(archive))
@@ -461,7 +480,7 @@ class TestFileHandlingWithArchive(DirectoriesMixin, TestCase):
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
Path(original).touch()
Path(archive).touch()
doc = Document.objects.create(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", checksum="A", archive_checksum="B")
doc = Document.objects.create(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", checksum="A", archive_checksum="B", archive_filename="0000001.pdf")
self.assertFalse(os.path.isfile(original))
self.assertFalse(os.path.isfile(archive))
@@ -475,7 +494,7 @@ class TestFileHandlingWithArchive(DirectoriesMixin, TestCase):
original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
Path(original).touch()
doc = Document.objects.create(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", checksum="A", archive_checksum="B")
doc = Document.objects.create(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", checksum="A", archive_checksum="B", archive_filename="0000001.pdf")
self.assertTrue(os.path.isfile(original))
self.assertFalse(os.path.isfile(archive))
@@ -486,14 +505,49 @@ class TestFileHandlingWithArchive(DirectoriesMixin, TestCase):
def test_move_archive_exists(self):
original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
existing_archive_file = os.path.join(settings.ARCHIVE_DIR, "none", "my_doc.pdf")
Path(original).touch()
Path(archive).touch()
os.makedirs(os.path.join(settings.ARCHIVE_DIR, "none"))
Path(os.path.join(settings.ARCHIVE_DIR, "none", "my_doc.pdf")).touch()
doc = Document.objects.create(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", checksum="A", archive_checksum="B")
Path(existing_archive_file).touch()
doc = Document.objects.create(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", checksum="A", archive_checksum="B", archive_filename="0000001.pdf")
self.assertFalse(os.path.isfile(original))
self.assertFalse(os.path.isfile(archive))
self.assertTrue(os.path.isfile(doc.source_path))
self.assertTrue(os.path.isfile(doc.archive_path))
self.assertTrue(os.path.isfile(existing_archive_file))
self.assertEqual(doc.archive_filename, "none/my_doc_01.pdf")
@override_settings(PAPERLESS_FILENAME_FORMAT="{title}")
def test_move_original_only(self):
original = os.path.join(settings.ORIGINALS_DIR, "document_01.pdf")
archive = os.path.join(settings.ARCHIVE_DIR, "document.pdf")
Path(original).touch()
Path(archive).touch()
doc = Document.objects.create(mime_type="application/pdf", title="document", filename="document_01.pdf", checksum="A",
archive_checksum="B", archive_filename="document.pdf")
self.assertEqual(doc.filename, "document.pdf")
self.assertEqual(doc.archive_filename, "document.pdf")
self.assertTrue(os.path.isfile(doc.source_path))
self.assertTrue(os.path.isfile(doc.archive_path))
@override_settings(PAPERLESS_FILENAME_FORMAT="{title}")
def test_move_archive_only(self):
original = os.path.join(settings.ORIGINALS_DIR, "document.pdf")
archive = os.path.join(settings.ARCHIVE_DIR, "document_01.pdf")
Path(original).touch()
Path(archive).touch()
doc = Document.objects.create(mime_type="application/pdf", title="document", filename="document.pdf", checksum="A",
archive_checksum="B", archive_filename="document_01.pdf")
self.assertEqual(doc.filename, "document.pdf")
self.assertEqual(doc.archive_filename, "document.pdf")
self.assertTrue(os.path.isfile(original))
self.assertTrue(os.path.isfile(archive))
self.assertTrue(os.path.isfile(doc.source_path))
self.assertTrue(os.path.isfile(doc.archive_path))
@@ -514,8 +568,9 @@ class TestFileHandlingWithArchive(DirectoriesMixin, TestCase):
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
Path(original).touch()
Path(archive).touch()
doc = Document.objects.create(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", checksum="A", archive_checksum="B")
doc = Document.objects.create(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", checksum="A", archive_checksum="B", archive_filename="0000001.pdf")
m.assert_called()
self.assertTrue(os.path.isfile(original))
self.assertTrue(os.path.isfile(archive))
self.assertTrue(os.path.isfile(doc.source_path))
@@ -527,7 +582,7 @@ class TestFileHandlingWithArchive(DirectoriesMixin, TestCase):
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
#Path(original).touch()
Path(archive).touch()
doc = Document.objects.create(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", checksum="A", archive_checksum="B")
doc = Document.objects.create(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", archive_filename="0000001.pdf", checksum="A", archive_checksum="B")
self.assertFalse(os.path.isfile(original))
self.assertTrue(os.path.isfile(archive))
@@ -551,19 +606,21 @@ class TestFileHandlingWithArchive(DirectoriesMixin, TestCase):
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
Path(original).touch()
Path(archive).touch()
doc = Document.objects.create(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", checksum="A", archive_checksum="B")
doc = Document.objects.create(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", archive_filename="0000001.pdf", checksum="A", archive_checksum="B")
m.assert_called()
self.assertTrue(os.path.isfile(original))
self.assertTrue(os.path.isfile(archive))
self.assertTrue(os.path.isfile(doc.source_path))
self.assertTrue(os.path.isfile(doc.archive_path))
@override_settings(PAPERLESS_FILENAME_FORMAT="")
def test_archive_deleted(self):
original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
Path(original).touch()
Path(archive).touch()
doc = Document.objects.create(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", checksum="A", archive_checksum="B")
doc = Document.objects.create(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", checksum="A", archive_checksum="B", archive_filename="0000001.pdf")
self.assertTrue(os.path.isfile(original))
self.assertTrue(os.path.isfile(archive))
@@ -577,6 +634,28 @@ class TestFileHandlingWithArchive(DirectoriesMixin, TestCase):
self.assertFalse(os.path.isfile(doc.source_path))
self.assertFalse(os.path.isfile(doc.archive_path))
@override_settings(PAPERLESS_FILENAME_FORMAT="{title}")
def test_archive_deleted2(self):
original = os.path.join(settings.ORIGINALS_DIR, "document.png")
original2 = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
Path(original).touch()
Path(original2).touch()
Path(archive).touch()
doc1 = Document.objects.create(mime_type="image/png", title="document", filename="document.png", checksum="A", archive_checksum="B", archive_filename="0000001.pdf")
doc2 = Document.objects.create(mime_type="application/pdf", title="0000001", filename="0000001.pdf", checksum="C")
self.assertTrue(os.path.isfile(doc1.source_path))
self.assertTrue(os.path.isfile(doc1.archive_path))
self.assertTrue(os.path.isfile(doc2.source_path))
doc2.delete()
self.assertTrue(os.path.isfile(doc1.source_path))
self.assertTrue(os.path.isfile(doc1.archive_path))
self.assertFalse(os.path.isfile(doc2.source_path))
@override_settings(PAPERLESS_FILENAME_FORMAT="{correspondent}/{title}")
def test_database_error(self):
@@ -584,7 +663,7 @@ class TestFileHandlingWithArchive(DirectoriesMixin, TestCase):
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
Path(original).touch()
Path(archive).touch()
doc = Document(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", checksum="A", archive_checksum="B")
doc = Document(mime_type="application/pdf", title="my_doc", filename="0000001.pdf", checksum="A", archive_filename="0000001.pdf", archive_checksum="B")
with mock.patch("documents.signals.handlers.Document.objects.filter") as m:
m.side_effect = DatabaseError()
doc.save()
@@ -594,6 +673,7 @@ class TestFileHandlingWithArchive(DirectoriesMixin, TestCase):
self.assertTrue(os.path.isfile(doc.source_path))
self.assertTrue(os.path.isfile(doc.archive_path))
class TestFilenameGeneration(TestCase):
@override_settings(
@@ -617,7 +697,7 @@ class TestFilenameGeneration(TestCase):
def run():
doc = Document.objects.create(checksum=str(uuid.uuid4()), title=str(uuid.uuid4()), content="wow")
doc.filename = generate_unique_filename(doc, settings.ORIGINALS_DIR)
doc.filename = generate_unique_filename(doc)
Path(doc.thumbnail_path).touch()
with open(doc.source_path, "w") as f:
f.write(str(uuid.uuid4()))

View File

@@ -1,20 +1,10 @@
from django.test import TestCase
from documents import index
from documents.index import JsonFormatter
from documents.models import Document
from documents.tests.utils import DirectoriesMixin
class JsonFormatterTest(TestCase):
def setUp(self) -> None:
self.formatter = JsonFormatter()
def test_empty_fragments(self):
self.assertListEqual(self.formatter.format([]), [])
class TestAutoComplete(DirectoriesMixin, TestCase):
def test_auto_complete(self):

View File

@@ -1,66 +0,0 @@
import logging
import uuid
from unittest import mock
from django.test import TestCase, override_settings
from ..models import Log
class TestPaperlessLog(TestCase):
def __init__(self, *args, **kwargs):
TestCase.__init__(self, *args, **kwargs)
self.logger = logging.getLogger(
"documents.management.commands.document_consumer")
@override_settings(DISABLE_DBHANDLER=False)
def test_that_it_saves_at_all(self):
kw = {"group": uuid.uuid4()}
self.assertEqual(Log.objects.all().count(), 0)
with mock.patch("logging.StreamHandler.emit") as __:
# Debug messages are ignored by default
self.logger.debug("This is a debugging message", extra=kw)
self.assertEqual(Log.objects.all().count(), 1)
self.logger.info("This is an informational message", extra=kw)
self.assertEqual(Log.objects.all().count(), 2)
self.logger.warning("This is an warning message", extra=kw)
self.assertEqual(Log.objects.all().count(), 3)
self.logger.error("This is an error message", extra=kw)
self.assertEqual(Log.objects.all().count(), 4)
self.logger.critical("This is a critical message", extra=kw)
self.assertEqual(Log.objects.all().count(), 5)
@override_settings(DISABLE_DBHANDLER=False)
def test_groups(self):
kw1 = {"group": uuid.uuid4()}
kw2 = {"group": uuid.uuid4()}
self.assertEqual(Log.objects.all().count(), 0)
with mock.patch("logging.StreamHandler.emit") as __:
self.logger.info("This is an informational message", extra=kw2)
self.assertEqual(Log.objects.all().count(), 1)
self.assertEqual(Log.objects.filter(group=kw2["group"]).count(), 1)
self.logger.warning("This is an warning message", extra=kw1)
self.assertEqual(Log.objects.all().count(), 2)
self.assertEqual(Log.objects.filter(group=kw1["group"]).count(), 1)
self.logger.error("This is an error message", extra=kw2)
self.assertEqual(Log.objects.all().count(), 3)
self.assertEqual(Log.objects.filter(group=kw2["group"]).count(), 2)
self.logger.critical("This is a critical message", extra=kw1)
self.assertEqual(Log.objects.all().count(), 4)
self.assertEqual(Log.objects.filter(group=kw1["group"]).count(), 2)

View File

@@ -20,6 +20,7 @@ from documents.tests.utils import DirectoriesMixin
sample_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf")
@override_settings(PAPERLESS_FILENAME_FORMAT="{correspondent}/{title}")
class TestArchiver(DirectoriesMixin, TestCase):
def make_models(self):
@@ -42,9 +43,42 @@ class TestArchiver(DirectoriesMixin, TestCase):
doc = Document.objects.get(id=doc.id)
self.assertIsNotNone(doc.checksum)
self.assertIsNotNone(doc.archive_checksum)
self.assertTrue(os.path.isfile(doc.archive_path))
self.assertTrue(os.path.isfile(doc.source_path))
self.assertTrue(filecmp.cmp(sample_file, doc.source_path))
self.assertEqual(doc.archive_filename, "none/A.pdf")
def test_unknown_mime_type(self):
doc = self.make_models()
doc.mime_type = "sdgfh"
doc.save()
shutil.copy(sample_file, doc.source_path)
handle_document(doc.pk)
doc = Document.objects.get(id=doc.id)
self.assertIsNotNone(doc.checksum)
self.assertIsNone(doc.archive_checksum)
self.assertIsNone(doc.archive_filename)
self.assertTrue(os.path.isfile(doc.source_path))
@override_settings(PAPERLESS_FILENAME_FORMAT="{title}")
def test_naming_priorities(self):
doc1 = Document.objects.create(checksum="A", title="document", content="first document", mime_type="application/pdf", filename="document.pdf")
doc2 = Document.objects.create(checksum="B", title="document", content="second document", mime_type="application/pdf", filename="document_01.pdf")
shutil.copy(sample_file, os.path.join(self.dirs.originals_dir, f"document.pdf"))
shutil.copy(sample_file, os.path.join(self.dirs.originals_dir, f"document_01.pdf"))
handle_document(doc2.pk)
handle_document(doc1.pk)
doc1 = Document.objects.get(id=doc1.id)
doc2 = Document.objects.get(id=doc2.id)
self.assertEqual(doc1.archive_filename, "document.pdf")
self.assertEqual(doc2.archive_filename, "document_01.pdf")
class TestDecryptDocuments(TestCase):
@@ -106,24 +140,27 @@ class TestMakeIndex(TestCase):
class TestRenamer(DirectoriesMixin, TestCase):
@override_settings(PAPERLESS_FILENAME_FORMAT="")
def test_rename(self):
doc = Document.objects.create(title="test", mime_type="application/pdf")
doc = Document.objects.create(title="test", mime_type="image/jpeg")
doc.filename = generate_filename(doc)
doc.archive_filename = generate_filename(doc, archive_filename=True)
doc.save()
Path(doc.source_path).touch()
Path(doc.archive_path).touch()
old_source_path = doc.source_path
with override_settings(PAPERLESS_FILENAME_FORMAT="{title}"):
with override_settings(PAPERLESS_FILENAME_FORMAT="{correspondent}/{title}"):
call_command("document_renamer")
doc2 = Document.objects.get(id=doc.id)
self.assertEqual(doc2.filename, "test.pdf")
self.assertFalse(os.path.isfile(old_source_path))
self.assertEqual(doc2.filename, "none/test.jpg")
self.assertEqual(doc2.archive_filename, "none/test.pdf")
self.assertFalse(os.path.isfile(doc.source_path))
self.assertFalse(os.path.isfile(doc.archive_path))
self.assertTrue(os.path.isfile(doc2.source_path))
self.assertTrue(os.path.isfile(doc2.archive_path))
class TestCreateClassifier(TestCase):
@@ -133,3 +170,24 @@ class TestCreateClassifier(TestCase):
call_command("document_create_classifier")
m.assert_called_once()
class TestSanityChecker(DirectoriesMixin, TestCase):
def test_no_issues(self):
with self.assertLogs() as capture:
call_command("document_sanity_checker")
self.assertEqual(len(capture.output), 1)
self.assertIn("Sanity checker detected no issues.", capture.output[0])
def test_errors(self):
doc = Document.objects.create(title="test", content="test", filename="test.pdf", checksum="abc")
Path(doc.source_path).touch()
Path(doc.thumbnail_path).touch()
with self.assertLogs() as capture:
call_command("document_sanity_checker")
self.assertEqual(len(capture.output), 1)
self.assertIn("Checksum mismatch of document", capture.output[0])

View File

@@ -60,10 +60,10 @@ class ConsumerMixin:
super(ConsumerMixin, self).tearDown()
def wait_for_task_mock_call(self):
def wait_for_task_mock_call(self, excpeted_call_count=1):
n = 0
while n < 100:
if self.task_mock.call_count > 0:
if self.task_mock.call_count >= excpeted_call_count:
# give task_mock some time to finish and raise errors
sleep(1)
return
@@ -202,8 +202,44 @@ class TestConsumer(DirectoriesMixin, ConsumerMixin, TransactionTestCase):
self.assertRaises(CommandError, call_command, 'document_consumer', '--oneshot')
def test_mac_write(self):
self.task_mock.side_effect = self.bogus_task
@override_settings(CONSUMER_POLLING=1)
self.t_start()
shutil.copy(self.sample_file, os.path.join(self.dirs.consumption_dir, ".DS_STORE"))
shutil.copy(self.sample_file, os.path.join(self.dirs.consumption_dir, "my_file.pdf"))
shutil.copy(self.sample_file, os.path.join(self.dirs.consumption_dir, "._my_file.pdf"))
shutil.copy(self.sample_file, os.path.join(self.dirs.consumption_dir, "my_second_file.pdf"))
shutil.copy(self.sample_file, os.path.join(self.dirs.consumption_dir, "._my_second_file.pdf"))
sleep(5)
self.wait_for_task_mock_call(excpeted_call_count=2)
self.assertEqual(2, self.task_mock.call_count)
fnames = [os.path.basename(args[1]) for args, _ in self.task_mock.call_args_list]
self.assertCountEqual(fnames, ["my_file.pdf", "my_second_file.pdf"])
def test_is_ignored(self):
test_paths = [
(os.path.join(self.dirs.consumption_dir, "foo.pdf"), False),
(os.path.join(self.dirs.consumption_dir, "foo","bar.pdf"), False),
(os.path.join(self.dirs.consumption_dir, ".DS_STORE", "foo.pdf"), True),
(os.path.join(self.dirs.consumption_dir, "foo", ".DS_STORE", "bar.pdf"), True),
(os.path.join(self.dirs.consumption_dir, ".stfolder", "foo.pdf"), True),
(os.path.join(self.dirs.consumption_dir, "._foo.pdf"), True),
(os.path.join(self.dirs.consumption_dir, "._foo", "bar.pdf"), False),
]
for file_path, expected_ignored in test_paths:
self.assertEqual(
expected_ignored,
document_consumer._is_ignored(file_path),
f'_is_ignored("{file_path}") != {expected_ignored}')
@override_settings(CONSUMER_POLLING=1, CONSUMER_POLLING_DELAY=1, CONSUMER_POLLING_RETRY_COUNT=20)
class TestConsumerPolling(TestConsumer):
# just do all the tests with polling
pass
@@ -215,8 +251,7 @@ class TestConsumerRecursive(TestConsumer):
pass
@override_settings(CONSUMER_RECURSIVE=True)
@override_settings(CONSUMER_POLLING=1)
@override_settings(CONSUMER_RECURSIVE=True, CONSUMER_POLLING=1, CONSUMER_POLLING_DELAY=1, CONSUMER_POLLING_RETRY_COUNT=20)
class TestConsumerRecursivePolling(TestConsumer):
# just do all the tests with polling and recursive
pass
@@ -257,6 +292,6 @@ class TestConsumerTags(DirectoriesMixin, ConsumerMixin, TransactionTestCase):
# their order.
self.assertCountEqual(kwargs["override_tag_ids"], tag_ids)
@override_settings(CONSUMER_POLLING=1)
@override_settings(CONSUMER_POLLING=1, CONSUMER_POLLING_DELAY=1, CONSUMER_POLLING_RETRY_COUNT=20)
def test_consume_file_with_path_tags_polling(self):
self.test_consume_file_with_path_tags()

View File

@@ -22,7 +22,7 @@ class TestExportImport(DirectoriesMixin, TestCase):
self.target = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, self.target)
self.d1 = Document.objects.create(content="Content", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", title="wow1", filename="0000001.pdf", mime_type="application/pdf")
self.d1 = Document.objects.create(content="Content", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", title="wow1", filename="0000001.pdf", mime_type="application/pdf", archive_filename="0000001.pdf")
self.d2 = Document.objects.create(content="Content", checksum="9c9691e51741c1f4f41a20896af31770", title="wow2", filename="0000002.pdf", mime_type="application/pdf")
self.d3 = Document.objects.create(content="Content", checksum="d38d7ed02e988e072caf924e0f3fcb76", title="wow2", filename="0000003.pdf", mime_type="application/pdf")
self.d4 = Document.objects.create(content="Content", checksum="82186aaa94f0b98697d704b90fd1c072", title="wow_dec", filename="0000004.pdf.gpg", mime_type="application/pdf", storage_type=Document.STORAGE_TYPE_GPG)
@@ -69,7 +69,7 @@ class TestExportImport(DirectoriesMixin, TestCase):
manifest = self._do_export(use_filename_format=use_filename_format)
self.assertEqual(len(manifest), 7)
self.assertEqual(len(manifest), 8)
self.assertEqual(len(list(filter(lambda e: e['model'] == 'documents.document', manifest))), 4)
self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json")))

View File

@@ -11,14 +11,17 @@ class TestRetagger(DirectoriesMixin, TestCase):
self.d1 = Document.objects.create(checksum="A", title="A", content="first document")
self.d2 = Document.objects.create(checksum="B", title="B", content="second document")
self.d3 = Document.objects.create(checksum="C", title="C", content="unrelated document")
self.d4 = Document.objects.create(checksum="D", title="D", content="auto document")
self.tag_first = Tag.objects.create(name="tag1", match="first", matching_algorithm=Tag.MATCH_ANY)
self.tag_second = Tag.objects.create(name="tag2", match="second", matching_algorithm=Tag.MATCH_ANY)
self.tag_inbox = Tag.objects.create(name="test", is_inbox_tag=True)
self.tag_no_match = Tag.objects.create(name="test2")
self.tag_auto = Tag.objects.create(name="tagauto", matching_algorithm=Tag.MATCH_AUTO)
self.d3.tags.add(self.tag_inbox)
self.d3.tags.add(self.tag_no_match)
self.d4.tags.add(self.tag_auto)
self.correspondent_first = Correspondent.objects.create(
@@ -32,7 +35,8 @@ class TestRetagger(DirectoriesMixin, TestCase):
name="dt2", match="second", matching_algorithm=DocumentType.MATCH_ANY)
def get_updated_docs(self):
return Document.objects.get(title="A"), Document.objects.get(title="B"), Document.objects.get(title="C")
return Document.objects.get(title="A"), Document.objects.get(title="B"), \
Document.objects.get(title="C"), Document.objects.get(title="D")
def setUp(self) -> None:
super(TestRetagger, self).setUp()
@@ -40,25 +44,26 @@ class TestRetagger(DirectoriesMixin, TestCase):
def test_add_tags(self):
call_command('document_retagger', '--tags')
d_first, d_second, d_unrelated = self.get_updated_docs()
d_first, d_second, d_unrelated, d_auto = self.get_updated_docs()
self.assertEqual(d_first.tags.count(), 1)
self.assertEqual(d_second.tags.count(), 1)
self.assertEqual(d_unrelated.tags.count(), 2)
self.assertEqual(d_auto.tags.count(), 1)
self.assertEqual(d_first.tags.first(), self.tag_first)
self.assertEqual(d_second.tags.first(), self.tag_second)
def test_add_type(self):
call_command('document_retagger', '--document_type')
d_first, d_second, d_unrelated = self.get_updated_docs()
d_first, d_second, d_unrelated, d_auto = self.get_updated_docs()
self.assertEqual(d_first.document_type, self.doctype_first)
self.assertEqual(d_second.document_type, self.doctype_second)
def test_add_correspondent(self):
call_command('document_retagger', '--correspondent')
d_first, d_second, d_unrelated = self.get_updated_docs()
d_first, d_second, d_unrelated, d_auto = self.get_updated_docs()
self.assertEqual(d_first.correspondent, self.correspondent_first)
self.assertEqual(d_second.correspondent, self.correspondent_second)
@@ -68,11 +73,55 @@ class TestRetagger(DirectoriesMixin, TestCase):
call_command('document_retagger', '--tags', '--overwrite')
d_first, d_second, d_unrelated = self.get_updated_docs()
d_first, d_second, d_unrelated, d_auto = self.get_updated_docs()
self.assertIsNotNone(Tag.objects.get(id=self.tag_second.id))
self.assertCountEqual([tag.id for tag in d_first.tags.all()], [self.tag_first.id])
self.assertCountEqual([tag.id for tag in d_second.tags.all()], [self.tag_second.id])
self.assertCountEqual([tag.id for tag in d_unrelated.tags.all()], [self.tag_inbox.id, self.tag_no_match.id])
self.assertEqual(d_auto.tags.count(), 0)
def test_add_tags_suggest(self):
call_command('document_retagger', '--tags', '--suggest')
d_first, d_second, d_unrelated, d_auto = self.get_updated_docs()
self.assertEqual(d_first.tags.count(), 0)
self.assertEqual(d_second.tags.count(), 0)
self.assertEqual(d_auto.tags.count(), 1)
def test_add_type_suggest(self):
call_command('document_retagger', '--document_type', '--suggest')
d_first, d_second, d_unrelated, d_auto = self.get_updated_docs()
self.assertEqual(d_first.document_type, None)
self.assertEqual(d_second.document_type, None)
def test_add_correspondent_suggest(self):
call_command('document_retagger', '--correspondent', '--suggest')
d_first, d_second, d_unrelated, d_auto = self.get_updated_docs()
self.assertEqual(d_first.correspondent, None)
self.assertEqual(d_second.correspondent, None)
def test_add_tags_suggest_url(self):
call_command('document_retagger', '--tags', '--suggest', '--base-url=http://localhost')
d_first, d_second, d_unrelated, d_auto = self.get_updated_docs()
self.assertEqual(d_first.tags.count(), 0)
self.assertEqual(d_second.tags.count(), 0)
self.assertEqual(d_auto.tags.count(), 1)
def test_add_type_suggest_url(self):
call_command('document_retagger', '--document_type', '--suggest', '--base-url=http://localhost')
d_first, d_second, d_unrelated, d_auto = self.get_updated_docs()
self.assertEqual(d_first.document_type, None)
self.assertEqual(d_second.document_type, None)
def test_add_correspondent_suggest_url(self):
call_command('document_retagger', '--correspondent', '--suggest', '--base-url=http://localhost')
d_first, d_second, d_unrelated, d_auto = self.get_updated_docs()
self.assertEqual(d_first.correspondent, None)
self.assertEqual(d_second.correspondent, None)

View File

@@ -0,0 +1,66 @@
import os
import shutil
from unittest import mock
from django.contrib.auth.models import User
from django.core.management import call_command
from django.test import TestCase
from documents.management.commands.document_thumbnails import _process_document
from documents.models import Document, Tag, Correspondent, DocumentType
from documents.tests.utils import DirectoriesMixin
class TestManageSuperUser(DirectoriesMixin, TestCase):
def reset_environment(self):
if "PAPERLESS_ADMIN_USER" in os.environ:
del os.environ["PAPERLESS_ADMIN_USER"]
if "PAPERLESS_ADMIN_PASSWORD" in os.environ:
del os.environ["PAPERLESS_ADMIN_PASSWORD"]
def setUp(self) -> None:
super().setUp()
self.reset_environment()
def tearDown(self) -> None:
super().tearDown()
self.reset_environment()
def test_no_user(self):
call_command("manage_superuser")
# just the consumer user.
self.assertEqual(User.objects.count(), 1)
self.assertTrue(User.objects.filter(username="consumer").exists())
def test_create(self):
os.environ["PAPERLESS_ADMIN_USER"] = "new_user"
os.environ["PAPERLESS_ADMIN_PASSWORD"] = "123456"
call_command("manage_superuser")
user: User = User.objects.get_by_natural_key("new_user")
self.assertTrue(user.check_password("123456"))
def test_update(self):
os.environ["PAPERLESS_ADMIN_USER"] = "new_user"
os.environ["PAPERLESS_ADMIN_PASSWORD"] = "123456"
call_command("manage_superuser")
os.environ["PAPERLESS_ADMIN_USER"] = "new_user"
os.environ["PAPERLESS_ADMIN_PASSWORD"] = "more_secure_pwd_7645"
call_command("manage_superuser")
user: User = User.objects.get_by_natural_key("new_user")
self.assertTrue(user.check_password("more_secure_pwd_7645"))
def test_no_password(self):
os.environ["PAPERLESS_ADMIN_USER"] = "new_user"
call_command("manage_superuser")
with self.assertRaises(User.DoesNotExist):
User.objects.get_by_natural_key("new_user")

View File

@@ -0,0 +1,325 @@
import hashlib
import os
import shutil
from pathlib import Path
from unittest import mock
from django.conf import settings
from django.test import override_settings
from documents.parsers import ParseError
from documents.tests.utils import DirectoriesMixin, TestMigrations
STORAGE_TYPE_GPG = "gpg"
def archive_name_from_filename(filename):
return os.path.splitext(filename)[0] + ".pdf"
def archive_path_old(self):
if self.filename:
fname = archive_name_from_filename(self.filename)
else:
fname = "{:07}.pdf".format(self.pk)
return os.path.join(
settings.ARCHIVE_DIR,
fname
)
def archive_path_new(doc):
if doc.archive_filename is not None:
return os.path.join(
settings.ARCHIVE_DIR,
str(doc.archive_filename)
)
else:
return None
def source_path(doc):
if doc.filename:
fname = str(doc.filename)
else:
fname = "{:07}{}".format(doc.pk, doc.file_type)
if doc.storage_type == STORAGE_TYPE_GPG:
fname += ".gpg" # pragma: no cover
return os.path.join(
settings.ORIGINALS_DIR,
fname
)
def thumbnail_path(doc):
file_name = "{:07}.png".format(doc.pk)
if doc.storage_type == STORAGE_TYPE_GPG:
file_name += ".gpg"
return os.path.join(
settings.THUMBNAIL_DIR,
file_name
)
def make_test_document(document_class, title: str, mime_type: str, original: str, original_filename: str, archive: str = None, archive_filename: str = None):
doc = document_class()
doc.filename = original_filename
doc.title = title
doc.mime_type = mime_type
doc.content = "the content, does not matter for this test"
doc.save()
shutil.copy2(original, source_path(doc))
with open(original, "rb") as f:
doc.checksum = hashlib.md5(f.read()).hexdigest()
if archive:
if archive_filename:
doc.archive_filename = archive_filename
shutil.copy2(archive, archive_path_new(doc))
else:
shutil.copy2(archive, archive_path_old(doc))
with open(archive, "rb") as f:
doc.archive_checksum = hashlib.md5(f.read()).hexdigest()
doc.save()
Path(thumbnail_path(doc)).touch()
return doc
simple_jpg = os.path.join(os.path.dirname(__file__), "samples", "simple.jpg")
simple_pdf = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf")
simple_pdf2 = os.path.join(os.path.dirname(__file__), "samples", "documents", "originals", "0000002.pdf")
simple_pdf3 = os.path.join(os.path.dirname(__file__), "samples", "documents", "originals", "0000003.pdf")
simple_txt = os.path.join(os.path.dirname(__file__), "samples", "simple.txt")
simple_png = os.path.join(os.path.dirname(__file__), "samples", "simple-noalpha.png")
simple_png2 = os.path.join(os.path.dirname(__file__), "examples", "no-text.png")
@override_settings(PAPERLESS_FILENAME_FORMAT="")
class TestMigrateArchiveFiles(DirectoriesMixin, TestMigrations):
migrate_from = '1011_auto_20210101_2340'
migrate_to = '1012_fix_archive_files'
def setUpBeforeMigration(self, apps):
Document = apps.get_model("documents", "Document")
self.unrelated = make_test_document(Document, "unrelated", "application/pdf", simple_pdf3, "unrelated.pdf", simple_pdf)
self.no_text = make_test_document(Document, "no-text", "image/png", simple_png2, "no-text.png", simple_pdf)
self.doc_no_archive = make_test_document(Document, "no_archive", "text/plain", simple_txt, "no_archive.txt")
self.clash1 = make_test_document(Document, "clash", "application/pdf", simple_pdf, "clash.pdf", simple_pdf)
self.clash2 = make_test_document(Document, "clash", "image/jpeg", simple_jpg, "clash.jpg", simple_pdf)
self.clash3 = make_test_document(Document, "clash", "image/png", simple_png, "clash.png", simple_pdf)
self.clash4 = make_test_document(Document, "clash.png", "application/pdf", simple_pdf2, "clash.png.pdf", simple_pdf2)
self.assertEqual(archive_path_old(self.clash1), archive_path_old(self.clash2))
self.assertEqual(archive_path_old(self.clash1), archive_path_old(self.clash3))
self.assertNotEqual(archive_path_old(self.clash1), archive_path_old(self.clash4))
def testArchiveFilesMigrated(self):
Document = self.apps.get_model('documents', 'Document')
for doc in Document.objects.all():
if doc.archive_checksum:
self.assertIsNotNone(doc.archive_filename)
self.assertTrue(os.path.isfile(archive_path_new(doc)))
else:
self.assertIsNone(doc.archive_filename)
with open(source_path(doc), "rb") as f:
original_checksum = hashlib.md5(f.read()).hexdigest()
self.assertEqual(original_checksum, doc.checksum)
if doc.archive_checksum:
self.assertTrue(os.path.isfile(archive_path_new(doc)))
with open(archive_path_new(doc), "rb") as f:
archive_checksum = hashlib.md5(f.read()).hexdigest()
self.assertEqual(archive_checksum, doc.archive_checksum)
self.assertEqual(Document.objects.filter(archive_checksum__isnull=False).count(), 6)
def test_filenames(self):
Document = self.apps.get_model('documents', 'Document')
self.assertEqual(Document.objects.get(id=self.unrelated.id).archive_filename, "unrelated.pdf")
self.assertEqual(Document.objects.get(id=self.no_text.id).archive_filename, "no-text.pdf")
self.assertEqual(Document.objects.get(id=self.doc_no_archive.id).archive_filename, None)
self.assertEqual(Document.objects.get(id=self.clash1.id).archive_filename, f"{self.clash1.id:07}.pdf")
self.assertEqual(Document.objects.get(id=self.clash2.id).archive_filename, f"{self.clash2.id:07}.pdf")
self.assertEqual(Document.objects.get(id=self.clash3.id).archive_filename, f"{self.clash3.id:07}.pdf")
self.assertEqual(Document.objects.get(id=self.clash4.id).archive_filename, "clash.png.pdf")
@override_settings(PAPERLESS_FILENAME_FORMAT="{correspondent}/{title}")
class TestMigrateArchiveFilesWithFilenameFormat(TestMigrateArchiveFiles):
def test_filenames(self):
Document = self.apps.get_model('documents', 'Document')
self.assertEqual(Document.objects.get(id=self.unrelated.id).archive_filename, "unrelated.pdf")
self.assertEqual(Document.objects.get(id=self.no_text.id).archive_filename, "no-text.pdf")
self.assertEqual(Document.objects.get(id=self.doc_no_archive.id).archive_filename, None)
self.assertEqual(Document.objects.get(id=self.clash1.id).archive_filename, "none/clash.pdf")
self.assertEqual(Document.objects.get(id=self.clash2.id).archive_filename, "none/clash_01.pdf")
self.assertEqual(Document.objects.get(id=self.clash3.id).archive_filename, "none/clash_02.pdf")
self.assertEqual(Document.objects.get(id=self.clash4.id).archive_filename, "clash.png.pdf")
def fake_parse_wrapper(parser, path, mime_type, file_name):
parser.archive_path = None
parser.text = "the text"
@override_settings(PAPERLESS_FILENAME_FORMAT="")
class TestMigrateArchiveFilesErrors(DirectoriesMixin, TestMigrations):
migrate_from = '1011_auto_20210101_2340'
migrate_to = '1012_fix_archive_files'
auto_migrate = False
def test_archive_missing(self):
Document = self.apps.get_model("documents", "Document")
doc = make_test_document(Document, "clash", "application/pdf", simple_pdf, "clash.pdf", simple_pdf)
os.unlink(archive_path_old(doc))
self.assertRaisesMessage(ValueError, "does not exist at: ", self.performMigration)
def test_parser_missing(self):
Document = self.apps.get_model("documents", "Document")
doc1 = make_test_document(Document, "document", "invalid/typesss768", simple_png, "document.png", simple_pdf)
doc2 = make_test_document(Document, "document", "invalid/typesss768", simple_jpg, "document.jpg", simple_pdf)
self.assertRaisesMessage(ValueError, "no parsers are available", self.performMigration)
@mock.patch("documents.migrations.1012_fix_archive_files.parse_wrapper")
def test_parser_error(self, m):
m.side_effect = ParseError()
Document = self.apps.get_model("documents", "Document")
doc1 = make_test_document(Document, "document", "image/png", simple_png, "document.png", simple_pdf)
doc2 = make_test_document(Document, "document", "application/pdf", simple_jpg, "document.jpg", simple_pdf)
self.assertIsNotNone(doc1.archive_checksum)
self.assertIsNotNone(doc2.archive_checksum)
with self.assertLogs() as capture:
self.performMigration()
self.assertEqual(m.call_count, 6)
self.assertEqual(
len(list(filter(lambda log: "Parse error, will try again in 5 seconds" in log, capture.output))),
4)
self.assertEqual(
len(list(filter(lambda log: "Unable to regenerate archive document for ID:" in log, capture.output))),
2)
Document = self.apps.get_model("documents", "Document")
doc1 = Document.objects.get(id=doc1.id)
doc2 = Document.objects.get(id=doc2.id)
self.assertIsNone(doc1.archive_checksum)
self.assertIsNone(doc2.archive_checksum)
self.assertIsNone(doc1.archive_filename)
self.assertIsNone(doc2.archive_filename)
@mock.patch("documents.migrations.1012_fix_archive_files.parse_wrapper")
def test_parser_no_archive(self, m):
m.side_effect = fake_parse_wrapper
Document = self.apps.get_model("documents", "Document")
doc1 = make_test_document(Document, "document", "image/png", simple_png, "document.png", simple_pdf)
doc2 = make_test_document(Document, "document", "application/pdf", simple_jpg, "document.jpg", simple_pdf)
with self.assertLogs() as capture:
self.performMigration()
self.assertEqual(
len(list(filter(lambda log: "Parser did not return an archive document for document" in log, capture.output))),
2)
Document = self.apps.get_model("documents", "Document")
doc1 = Document.objects.get(id=doc1.id)
doc2 = Document.objects.get(id=doc2.id)
self.assertIsNone(doc1.archive_checksum)
self.assertIsNone(doc2.archive_checksum)
self.assertIsNone(doc1.archive_filename)
self.assertIsNone(doc2.archive_filename)
@override_settings(PAPERLESS_FILENAME_FORMAT="")
class TestMigrateArchiveFilesBackwards(DirectoriesMixin, TestMigrations):
migrate_from = '1012_fix_archive_files'
migrate_to = '1011_auto_20210101_2340'
def setUpBeforeMigration(self, apps):
Document = apps.get_model("documents", "Document")
doc_unrelated = make_test_document(Document, "unrelated", "application/pdf", simple_pdf2, "unrelated.txt", simple_pdf2, "unrelated.pdf")
doc_no_archive = make_test_document(Document, "no_archive", "text/plain", simple_txt, "no_archive.txt")
clashB = make_test_document(Document, "clash", "image/jpeg", simple_jpg, "clash.jpg", simple_pdf, "clash_02.pdf")
def testArchiveFilesReverted(self):
Document = self.apps.get_model('documents', 'Document')
for doc in Document.objects.all():
if doc.archive_checksum:
self.assertTrue(os.path.isfile(archive_path_old(doc)))
with open(source_path(doc), "rb") as f:
original_checksum = hashlib.md5(f.read()).hexdigest()
self.assertEqual(original_checksum, doc.checksum)
if doc.archive_checksum:
self.assertTrue(os.path.isfile(archive_path_old(doc)))
with open(archive_path_old(doc), "rb") as f:
archive_checksum = hashlib.md5(f.read()).hexdigest()
self.assertEqual(archive_checksum, doc.archive_checksum)
self.assertEqual(Document.objects.filter(archive_checksum__isnull=False).count(), 2)
@override_settings(PAPERLESS_FILENAME_FORMAT="{correspondent}/{title}")
class TestMigrateArchiveFilesBackwardsWithFilenameFormat(TestMigrateArchiveFilesBackwards):
pass
@override_settings(PAPERLESS_FILENAME_FORMAT="")
class TestMigrateArchiveFilesBackwardsErrors(DirectoriesMixin, TestMigrations):
migrate_from = '1012_fix_archive_files'
migrate_to = '1011_auto_20210101_2340'
auto_migrate = False
def test_filename_clash(self):
Document = self.apps.get_model("documents", "Document")
self.clashA = make_test_document(Document, "clash", "application/pdf", simple_pdf, "clash.pdf", simple_pdf, "clash_02.pdf")
self.clashB = make_test_document(Document, "clash", "image/jpeg", simple_jpg, "clash.jpg", simple_pdf, "clash_01.pdf")
self.assertRaisesMessage(ValueError, "would clash with another archive filename", self.performMigration)
def test_filename_exists(self):
Document = self.apps.get_model("documents", "Document")
self.clashA = make_test_document(Document, "clash", "application/pdf", simple_pdf, "clash.pdf", simple_pdf, "clash.pdf")
self.clashB = make_test_document(Document, "clash", "image/jpeg", simple_jpg, "clash.jpg", simple_pdf, "clash_01.pdf")
self.assertRaisesMessage(ValueError, "file already exists.", self.performMigration)

View File

@@ -1,52 +1,11 @@
import os
import shutil
from pathlib import Path
from django.apps import apps
from django.conf import settings
from django.db import connection
from django.db.migrations.executor import MigrationExecutor
from django.test import TestCase, TransactionTestCase, override_settings
from django.test import override_settings
from documents.models import Document
from documents.parsers import get_default_file_extension
from documents.tests.utils import DirectoriesMixin
class TestMigrations(TransactionTestCase):
@property
def app(self):
return apps.get_containing_app_config(type(self).__module__).name
migrate_from = None
migrate_to = None
def setUp(self):
super(TestMigrations, self).setUp()
assert self.migrate_from and self.migrate_to, \
"TestCase '{}' must define migrate_from and migrate_to properties".format(type(self).__name__)
self.migrate_from = [(self.app, self.migrate_from)]
self.migrate_to = [(self.app, self.migrate_to)]
executor = MigrationExecutor(connection)
old_apps = executor.loader.project_state(self.migrate_from).apps
# Reverse to the original migration
executor.migrate(self.migrate_from)
self.setUpBeforeMigration(old_apps)
# Run the migration to test
executor = MigrationExecutor(connection)
executor.loader.build_graph() # reload.
executor.migrate(self.migrate_to)
self.apps = executor.loader.project_state(self.migrate_to).apps
def setUpBeforeMigration(self, apps):
pass
from documents.tests.utils import DirectoriesMixin, TestMigrations
STORAGE_TYPE_UNENCRYPTED = "unencrypted"
STORAGE_TYPE_GPG = "gpg"

View File

@@ -0,0 +1,15 @@
from documents.tests.utils import DirectoriesMixin, TestMigrations
class TestMigrateNullCharacters(DirectoriesMixin, TestMigrations):
migrate_from = '1014_auto_20210228_1614'
migrate_to = '1015_remove_null_characters'
def setUpBeforeMigration(self, apps):
Document = apps.get_model("documents", "Document")
self.doc = Document.objects.create(content="aaa\0bbb")
def testMimeTypesMigrated(self):
Document = self.apps.get_model('documents', 'Document')
self.assertNotIn("\0", Document.objects.get(id=self.doc.id).content)

View File

@@ -0,0 +1,37 @@
from documents.tests.utils import DirectoriesMixin, TestMigrations
class TestMigrateTagColor(DirectoriesMixin, TestMigrations):
migrate_from = '1012_fix_archive_files'
migrate_to = '1013_migrate_tag_colour'
def setUpBeforeMigration(self, apps):
Tag = apps.get_model("documents", "Tag")
self.t1_id = Tag.objects.create(name="tag1").id
self.t2_id = Tag.objects.create(name="tag2", colour=1).id
self.t3_id = Tag.objects.create(name="tag3", colour=5).id
def testMimeTypesMigrated(self):
Tag = self.apps.get_model('documents', 'Tag')
self.assertEqual(Tag.objects.get(id=self.t1_id).color, "#a6cee3")
self.assertEqual(Tag.objects.get(id=self.t2_id).color, "#a6cee3")
self.assertEqual(Tag.objects.get(id=self.t3_id).color, "#fb9a99")
class TestMigrateTagColorBackwards(DirectoriesMixin, TestMigrations):
migrate_from = '1013_migrate_tag_colour'
migrate_to = '1012_fix_archive_files'
def setUpBeforeMigration(self, apps):
Tag = apps.get_model("documents", "Tag")
self.t1_id = Tag.objects.create(name="tag1").id
self.t2_id = Tag.objects.create(name="tag2", color="#cab2d6").id
self.t3_id = Tag.objects.create(name="tag3", color="#123456").id
def testMimeTypesReverted(self):
Tag = self.apps.get_model('documents', 'Tag')
self.assertEqual(Tag.objects.get(id=self.t1_id).colour, 1)
self.assertEqual(Tag.objects.get(id=self.t2_id).colour, 9)
self.assertEqual(Tag.objects.get(id=self.t3_id).colour, 1)

View File

@@ -68,7 +68,7 @@ class TestParserDiscovery(TestCase):
)
def fake_get_thumbnail(self, path, mimetype):
def fake_get_thumbnail(self, path, mimetype, file_name):
return os.path.join(os.path.dirname(__file__), "examples", "no-text.png")
@@ -89,15 +89,15 @@ class TestBaseParser(TestCase):
def test_get_optimised_thumbnail(self):
parser = DocumentParser(None)
parser.get_optimised_thumbnail("any", "not important")
parser.get_optimised_thumbnail("any", "not important", "document.pdf")
@mock.patch("documents.parsers.DocumentParser.get_thumbnail", fake_get_thumbnail)
@override_settings(OPTIMIZE_THUMBNAILS=False)
def test_get_optimised_thumb_disabled(self):
parser = DocumentParser(None)
path = parser.get_optimised_thumbnail("any", "not important")
self.assertEqual(path, fake_get_thumbnail(None, None, None))
path = parser.get_optimised_thumbnail("any", "not important", "document.pdf")
self.assertEqual(path, fake_get_thumbnail(None, None, None, None))
class TestParserAvailability(TestCase):
@@ -114,8 +114,8 @@ class TestParserAvailability(TestCase):
self.assertEqual(get_default_file_extension('application/zip'), ".zip")
self.assertEqual(get_default_file_extension('aasdasd/dgfgf'), "")
self.assertEqual(get_parser_class_for_mime_type('application/pdf'), RasterisedDocumentParser)
self.assertEqual(get_parser_class_for_mime_type('text/plain'), TextDocumentParser)
self.assertIsInstance(get_parser_class_for_mime_type('application/pdf')(logging_group=None), RasterisedDocumentParser)
self.assertIsInstance(get_parser_class_for_mime_type('text/plain')(logging_group=None), TextDocumentParser)
self.assertEqual(get_parser_class_for_mime_type('text/sdgsdf'), None)
self.assertTrue(is_file_ext_supported('.pdf'))

View File

@@ -1,3 +1,4 @@
import logging
import os
import shutil
from pathlib import Path
@@ -7,10 +8,59 @@ from django.conf import settings
from django.test import TestCase
from documents.models import Document
from documents.sanity_checker import check_sanity, SanityFailedError
from documents.sanity_checker import check_sanity, SanityCheckMessages
from documents.tests.utils import DirectoriesMixin
class TestSanityCheckMessages(TestCase):
def test_no_messages(self):
messages = SanityCheckMessages()
self.assertEqual(len(messages), 0)
self.assertFalse(messages.has_error())
self.assertFalse(messages.has_warning())
with self.assertLogs() as capture:
messages.log_messages()
self.assertEqual(len(capture.output), 1)
self.assertEqual(capture.records[0].levelno, logging.INFO)
self.assertEqual(capture.records[0].message, "Sanity checker detected no issues.")
def test_info(self):
messages = SanityCheckMessages()
messages.info("Something might be wrong")
self.assertEqual(len(messages), 1)
self.assertFalse(messages.has_error())
self.assertFalse(messages.has_warning())
with self.assertLogs() as capture:
messages.log_messages()
self.assertEqual(len(capture.output), 1)
self.assertEqual(capture.records[0].levelno, logging.INFO)
self.assertEqual(capture.records[0].message, "Something might be wrong")
def test_warning(self):
messages = SanityCheckMessages()
messages.warning("Something is wrong")
self.assertEqual(len(messages), 1)
self.assertFalse(messages.has_error())
self.assertTrue(messages.has_warning())
with self.assertLogs() as capture:
messages.log_messages()
self.assertEqual(len(capture.output), 1)
self.assertEqual(capture.records[0].levelno, logging.WARNING)
self.assertEqual(capture.records[0].message, "Something is wrong")
def test_error(self):
messages = SanityCheckMessages()
messages.error("Something is seriously wrong")
self.assertEqual(len(messages), 1)
self.assertTrue(messages.has_error())
self.assertFalse(messages.has_warning())
with self.assertLogs() as capture:
messages.log_messages()
self.assertEqual(len(capture.output), 1)
self.assertEqual(capture.records[0].levelno, logging.ERROR)
self.assertEqual(capture.records[0].message, "Something is seriously wrong")
class TestSanityCheck(DirectoriesMixin, TestCase):
def make_test_data(self):
@@ -21,7 +71,12 @@ class TestSanityCheck(DirectoriesMixin, TestCase):
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "archive", "0000001.pdf"), os.path.join(self.dirs.archive_dir, "0000001.pdf"))
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "thumbnails", "0000001.png"), os.path.join(self.dirs.thumbnail_dir, "0000001.png"))
return Document.objects.create(title="test", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", content="test", pk=1, filename="0000001.pdf", mime_type="application/pdf")
return Document.objects.create(title="test", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", content="test", pk=1, filename="0000001.pdf", mime_type="application/pdf", archive_filename="0000001.pdf")
def assertSanityError(self, messageRegex):
messages = check_sanity()
self.assertTrue(messages.has_error())
self.assertRegex(messages[0]['message'], messageRegex)
def test_no_docs(self):
self.assertEqual(len(check_sanity()), 0)
@@ -33,59 +88,75 @@ class TestSanityCheck(DirectoriesMixin, TestCase):
def test_no_thumbnail(self):
doc = self.make_test_data()
os.remove(doc.thumbnail_path)
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Thumbnail of document .* does not exist")
def test_thumbnail_no_access(self):
doc = self.make_test_data()
os.chmod(doc.thumbnail_path, 0o000)
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Cannot read thumbnail file of document")
os.chmod(doc.thumbnail_path, 0o777)
def test_no_original(self):
doc = self.make_test_data()
os.remove(doc.source_path)
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Original of document .* does not exist.")
def test_original_no_access(self):
doc = self.make_test_data()
os.chmod(doc.source_path, 0o000)
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Cannot read original file of document")
os.chmod(doc.source_path, 0o777)
def test_original_checksum_mismatch(self):
doc = self.make_test_data()
doc.checksum = "WOW"
doc.save()
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Checksum mismatch of document")
def test_no_archive(self):
doc = self.make_test_data()
os.remove(doc.archive_path)
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Archived version of document .* does not exist.")
def test_archive_no_access(self):
doc = self.make_test_data()
os.chmod(doc.archive_path, 0o000)
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Cannot read archive file of document")
os.chmod(doc.archive_path, 0o777)
def test_archive_checksum_mismatch(self):
doc = self.make_test_data()
doc.archive_checksum = "WOW"
doc.save()
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Checksum mismatch of archived document")
def test_empty_content(self):
doc = self.make_test_data()
doc.content = ""
doc.save()
self.assertEqual(len(check_sanity()), 1)
messages = check_sanity()
self.assertFalse(messages.has_error())
self.assertFalse(messages.has_warning())
self.assertEqual(len(messages), 1)
self.assertRegex(messages[0]['message'], "Document .* has no content.")
def test_orphaned_file(self):
doc = self.make_test_data()
Path(self.dirs.originals_dir, "orphaned").touch()
self.assertEqual(len(check_sanity()), 1)
messages = check_sanity()
self.assertFalse(messages.has_error())
self.assertTrue(messages.has_warning())
self.assertEqual(len(messages), 1)
self.assertRegex(messages[0]['message'], "Orphaned file in media dir")
def test_all(self):
Document.objects.create(title="test", checksum="dgfhj", archive_checksum="dfhg", content="", pk=1, filename="0000001.pdf")
string = str(SanityFailedError(check_sanity()))
def test_archive_filename_no_checksum(self):
doc = self.make_test_data()
doc.archive_checksum = None
doc.save()
self.assertSanityError("has an archive file, but its checksum is missing.")
def test_archive_checksum_no_filename(self):
doc = self.make_test_data()
doc.archive_filename = None
doc.save()
self.assertSanityError("has an archive file checksum, but no archive filename.")

View File

@@ -20,7 +20,7 @@ class TestSettings(TestCase):
self.assertEqual(default_threads, 1)
def test_workers_threads(self):
for i in range(2, 64):
for i in range(1, 64):
with mock.patch("paperless.settings.multiprocessing.cpu_count") as cpu_count:
cpu_count.return_value = i
@@ -31,4 +31,4 @@ class TestSettings(TestCase):
self.assertTrue(default_workers >= 1)
self.assertTrue(default_threads >= 1)
self.assertTrue(default_workers * default_threads < i, f"{i}")
self.assertTrue(default_workers * default_threads <= i, f"{i}")

View File

@@ -1,12 +1,13 @@
from datetime import datetime
import os
from unittest import mock
from django.conf import settings
from django.test import TestCase
from django.utils import timezone
from documents import tasks
from documents.models import Document
from documents.sanity_checker import SanityError, SanityFailedError
from documents.models import Document, Tag, Correspondent, DocumentType
from documents.sanity_checker import SanityCheckMessages, SanityCheckFailedException
from documents.tests.utils import DirectoriesMixin
@@ -22,20 +23,87 @@ class TestTasks(DirectoriesMixin, TestCase):
tasks.index_optimize()
def test_train_classifier(self):
@mock.patch("documents.tasks.load_classifier")
def test_train_classifier_no_auto_matching(self, load_classifier):
tasks.train_classifier()
load_classifier.assert_not_called()
@mock.patch("documents.tasks.load_classifier")
def test_train_classifier_with_auto_tag(self, load_classifier):
load_classifier.return_value = None
Tag.objects.create(matching_algorithm=Tag.MATCH_AUTO, name="test")
tasks.train_classifier()
load_classifier.assert_called_once()
self.assertFalse(os.path.isfile(settings.MODEL_FILE))
@mock.patch("documents.tasks.load_classifier")
def test_train_classifier_with_auto_type(self, load_classifier):
load_classifier.return_value = None
DocumentType.objects.create(matching_algorithm=Tag.MATCH_AUTO, name="test")
tasks.train_classifier()
load_classifier.assert_called_once()
self.assertFalse(os.path.isfile(settings.MODEL_FILE))
@mock.patch("documents.tasks.load_classifier")
def test_train_classifier_with_auto_correspondent(self, load_classifier):
load_classifier.return_value = None
Correspondent.objects.create(matching_algorithm=Tag.MATCH_AUTO, name="test")
tasks.train_classifier()
load_classifier.assert_called_once()
self.assertFalse(os.path.isfile(settings.MODEL_FILE))
def test_train_classifier(self):
c = Correspondent.objects.create(matching_algorithm=Tag.MATCH_AUTO, name="test")
doc = Document.objects.create(correspondent=c, content="test", title="test")
self.assertFalse(os.path.isfile(settings.MODEL_FILE))
tasks.train_classifier()
self.assertTrue(os.path.isfile(settings.MODEL_FILE))
mtime = os.stat(settings.MODEL_FILE).st_mtime
tasks.train_classifier()
self.assertTrue(os.path.isfile(settings.MODEL_FILE))
mtime2 = os.stat(settings.MODEL_FILE).st_mtime
self.assertEqual(mtime, mtime2)
doc.content = "test2"
doc.save()
tasks.train_classifier()
self.assertTrue(os.path.isfile(settings.MODEL_FILE))
mtime3 = os.stat(settings.MODEL_FILE).st_mtime
self.assertNotEqual(mtime2, mtime3)
@mock.patch("documents.tasks.sanity_checker.check_sanity")
def test_sanity_check(self, m):
m.return_value = []
tasks.sanity_check()
m.assert_called_once()
m.reset_mock()
m.return_value = [SanityError("")]
self.assertRaises(SanityFailedError, tasks.sanity_check)
def test_sanity_check_success(self, m):
m.return_value = SanityCheckMessages()
self.assertEqual(tasks.sanity_check(), "No issues detected.")
m.assert_called_once()
def test_culk_update_documents(self):
@mock.patch("documents.tasks.sanity_checker.check_sanity")
def test_sanity_check_error(self, m):
messages = SanityCheckMessages()
messages.error("Some error")
m.return_value = messages
self.assertRaises(SanityCheckFailedException, tasks.sanity_check)
m.assert_called_once()
@mock.patch("documents.tasks.sanity_checker.check_sanity")
def test_sanity_check_warning(self, m):
messages = SanityCheckMessages()
messages.warning("Some warning")
m.return_value = messages
self.assertEqual(tasks.sanity_check(), "Sanity check exited with warnings. See log.")
m.assert_called_once()
@mock.patch("documents.tasks.sanity_checker.check_sanity")
def test_sanity_check_info(self, m):
messages = SanityCheckMessages()
messages.info("Some info")
m.return_value = messages
self.assertEqual(tasks.sanity_check(), "Sanity check exited with infos. See log.")
m.assert_called_once()
def test_bulk_update_documents(self):
doc1 = Document.objects.create(title="test", content="my document", checksum="wow", added=timezone.now(),
created=timezone.now(), modified=timezone.now())

View File

@@ -15,7 +15,7 @@ class TestViews(TestCase):
def test_index(self):
self.client.force_login(self.user)
for (language_given, language_actual) in [("", "en-US"), ("en-US", "en-US"), ("de", "de"), ("en", "en-US"), ("en-us", "en-US"), ("fr", "fr"), ("jp", "en-US")]:
for (language_given, language_actual) in [("", "en-US"), ("en-US", "en-US"), ("de", "de-DE"), ("en", "en-US"), ("en-us", "en-US"), ("fr", "fr-FR"), ("jp", "en-US")]:
if language_given:
self.client.cookies.load({settings.LANGUAGE_COOKIE_NAME: language_given})
elif settings.LANGUAGE_COOKIE_NAME in self.client.cookies.keys():

View File

@@ -4,7 +4,10 @@ import tempfile
from collections import namedtuple
from contextlib import contextmanager
from django.test import override_settings
from django.apps import apps
from django.db import connection
from django.db.migrations.executor import MigrationExecutor
from django.test import override_settings, TransactionTestCase
def setup_directories():
@@ -19,12 +22,15 @@ def setup_directories():
dirs.originals_dir = os.path.join(dirs.media_dir, "documents", "originals")
dirs.thumbnail_dir = os.path.join(dirs.media_dir, "documents", "thumbnails")
dirs.archive_dir = os.path.join(dirs.media_dir, "documents", "archive")
dirs.logging_dir = os.path.join(dirs.data_dir, "log")
os.makedirs(dirs.index_dir, exist_ok=True)
os.makedirs(dirs.originals_dir, exist_ok=True)
os.makedirs(dirs.thumbnail_dir, exist_ok=True)
os.makedirs(dirs.archive_dir, exist_ok=True)
os.makedirs(dirs.logging_dir, exist_ok=True)
dirs.settings_override = override_settings(
DATA_DIR=dirs.data_dir,
SCRATCH_DIR=dirs.scratch_dir,
@@ -33,6 +39,7 @@ def setup_directories():
THUMBNAIL_DIR=dirs.thumbnail_dir,
ARCHIVE_DIR=dirs.archive_dir,
CONSUMPTION_DIR=dirs.consumption_dir,
LOGGING_DIR=dirs.logging_dir,
INDEX_DIR=dirs.index_dir,
MODEL_FILE=os.path.join(dirs.data_dir, "classification_model.pickle"),
MEDIA_LOCK=os.path.join(dirs.media_dir, "media.lock")
@@ -75,3 +82,45 @@ class DirectoriesMixin:
def tearDown(self) -> None:
super(DirectoriesMixin, self).tearDown()
remove_dirs(self.dirs)
class TestMigrations(TransactionTestCase):
@property
def app(self):
return apps.get_containing_app_config(type(self).__module__).name
migrate_from = None
migrate_to = None
auto_migrate = True
def setUp(self):
super(TestMigrations, self).setUp()
assert self.migrate_from and self.migrate_to, \
"TestCase '{}' must define migrate_from and migrate_to properties".format(type(self).__name__)
self.migrate_from = [(self.app, self.migrate_from)]
self.migrate_to = [(self.app, self.migrate_to)]
executor = MigrationExecutor(connection)
old_apps = executor.loader.project_state(self.migrate_from).apps
# Reverse to the original migration
executor.migrate(self.migrate_from)
self.setUpBeforeMigration(old_apps)
self.apps = old_apps
if self.auto_migrate:
self.performMigration()
def performMigration(self):
# Run the migration to test
executor = MigrationExecutor(connection)
executor.loader.build_graph() # reload.
executor.migrate(self.migrate_to)
self.apps = executor.loader.project_state(self.migrate_to).apps
def setUpBeforeMigration(self, apps):
pass

403
src/documents/views.py Executable file → Normal file
View File

@@ -1,6 +1,8 @@
import logging
import os
import tempfile
import uuid
import zipfile
from datetime import datetime
from time import mktime
@@ -15,7 +17,9 @@ from django_filters.rest_framework import DjangoFilterBackend
from django_q.tasks import async_task
from rest_framework import parsers
from rest_framework.decorators import action
from rest_framework.exceptions import NotFound
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.generics import GenericAPIView
from rest_framework.mixins import (
DestroyModelMixin,
ListModelMixin,
@@ -28,33 +32,40 @@ from rest_framework.views import APIView
from rest_framework.viewsets import (
GenericViewSet,
ModelViewSet,
ReadOnlyModelViewSet
ViewSet
)
import documents.index as index
from paperless.db import GnuPG
from paperless.views import StandardPagination
from .bulk_download import OriginalAndArchiveStrategy, OriginalsOnlyStrategy, \
ArchiveOnlyStrategy
from .classifier import load_classifier
from .filters import (
CorrespondentFilterSet,
DocumentFilterSet,
TagFilterSet,
DocumentTypeFilterSet,
LogFilterSet
DocumentTypeFilterSet
)
from .models import Correspondent, Document, Log, Tag, DocumentType, SavedView
from .matching import match_correspondents, match_tags, match_document_types
from .models import Correspondent, Document, Tag, DocumentType, SavedView
from .parsers import get_parser_class_for_mime_type
from .serialisers import (
CorrespondentSerializer,
DocumentSerializer,
LogSerializer,
TagSerializerVersion1,
TagSerializer,
DocumentTypeSerializer,
PostDocumentSerializer,
SavedViewSerializer,
BulkEditSerializer, SelectionDataSerializer
BulkEditSerializer,
DocumentListSerializer,
BulkDownloadSerializer
)
logger = logging.getLogger("paperless.api")
class IndexView(TemplateView):
template_name = "index.html"
@@ -81,6 +92,7 @@ class IndexView(TemplateView):
context['polyfills_js'] = f"frontend/{self.get_language()}/polyfills.js" # NOQA: E501
context['main_js'] = f"frontend/{self.get_language()}/main.js"
context['webmanifest'] = f"frontend/{self.get_language()}/manifest.webmanifest" # NOQA: E501
context['apple_touch_icon'] = f"frontend/{self.get_language()}/apple-touch-icon.png" # NOQA: E501
return context
@@ -110,7 +122,12 @@ class TagViewSet(ModelViewSet):
queryset = Tag.objects.annotate(
document_count=Count('documents')).order_by(Lower('name'))
serializer_class = TagSerializer
def get_serializer_class(self):
if int(self.request.version) == 1:
return TagSerializerVersion1
else:
return TagSerializer
pagination_class = StandardPagination
permission_classes = (IsAuthenticated,)
filter_backends = (DjangoFilterBackend, OrderingFilter)
@@ -132,10 +149,6 @@ class DocumentTypeViewSet(ModelViewSet):
ordering_fields = ("name", "matching_algorithm", "match", "document_count")
class BulkEditForm(object):
pass
class DocumentViewSet(RetrieveModelMixin,
UpdateModelMixin,
DestroyModelMixin,
@@ -159,6 +172,9 @@ class DocumentViewSet(RetrieveModelMixin,
"added",
"archive_serial_number")
def get_queryset(self):
return Document.objects.distinct()
def get_serializer(self, *args, **kwargs):
fields_param = self.request.query_params.get('fields', None)
if fields_param:
@@ -173,10 +189,12 @@ class DocumentViewSet(RetrieveModelMixin,
def update(self, request, *args, **kwargs):
response = super(DocumentViewSet, self).update(
request, *args, **kwargs)
from documents import index
index.add_or_update_document(self.get_object())
return response
def destroy(self, request, *args, **kwargs):
from documents import index
index.remove_document_from_index(self.get_object())
return super(DocumentViewSet, self).destroy(request, *args, **kwargs)
@@ -189,7 +207,7 @@ class DocumentViewSet(RetrieveModelMixin,
def file_response(self, pk, request, disposition):
doc = Document.objects.get(id=pk)
if not self.original_requested(request) and os.path.isfile(doc.archive_path): # NOQA: E501
if not self.original_requested(request) and doc.has_archive_version: # NOQA: E501
file_handle = doc.archive_file
filename = doc.get_public_filename(archive=True)
mime_type = 'application/pdf'
@@ -212,7 +230,7 @@ class DocumentViewSet(RetrieveModelMixin,
parser_class = get_parser_class_for_mime_type(mime_type)
if parser_class:
parser = parser_class(logging_group=None)
parser = parser_class(progress_callback=None, logging_group=None)
try:
return parser.extract_metadata(file, mime_type)
@@ -222,35 +240,60 @@ class DocumentViewSet(RetrieveModelMixin,
else:
return []
def get_filesize(self, filename):
if os.path.isfile(filename):
return os.stat(filename).st_size
else:
return None
@action(methods=['get'], detail=True)
def metadata(self, request, pk=None):
try:
doc = Document.objects.get(pk=pk)
meta = {
"original_checksum": doc.checksum,
"original_size": os.stat(doc.source_path).st_size,
"original_mime_type": doc.mime_type,
"media_filename": doc.filename,
"has_archive_version": os.path.isfile(doc.archive_path),
"original_metadata": self.get_metadata(
doc.source_path, doc.mime_type)
}
if doc.archive_checksum and os.path.isfile(doc.archive_path):
meta['archive_checksum'] = doc.archive_checksum
meta['archive_size'] = os.stat(doc.archive_path).st_size,
meta['archive_metadata'] = self.get_metadata(
doc.archive_path, "application/pdf")
else:
meta['archive_checksum'] = None
meta['archive_size'] = None
meta['archive_metadata'] = None
return Response(meta)
except Document.DoesNotExist:
raise Http404()
meta = {
"original_checksum": doc.checksum,
"original_size": self.get_filesize(doc.source_path),
"original_mime_type": doc.mime_type,
"media_filename": doc.filename,
"has_archive_version": doc.has_archive_version,
"original_metadata": self.get_metadata(
doc.source_path, doc.mime_type),
"archive_checksum": doc.archive_checksum,
"archive_media_filename": doc.archive_filename
}
if doc.has_archive_version:
meta['archive_size'] = self.get_filesize(doc.archive_path)
meta['archive_metadata'] = self.get_metadata(
doc.archive_path, "application/pdf")
else:
meta['archive_size'] = None
meta['archive_metadata'] = None
return Response(meta)
@action(methods=['get'], detail=True)
def suggestions(self, request, pk=None):
try:
doc = Document.objects.get(pk=pk)
except Document.DoesNotExist:
raise Http404()
classifier = load_classifier()
return Response({
"correspondents": [
c.id for c in match_correspondents(doc, classifier)
],
"tags": [t.id for t in match_tags(doc, classifier)],
"document_types": [
dt.id for dt in match_document_types(doc, classifier)
]
})
@action(methods=['get'], detail=True)
def preview(self, request, pk=None):
try:
@@ -269,6 +312,8 @@ class DocumentViewSet(RetrieveModelMixin,
handle = GnuPG.decrypted(doc.thumbnail_file)
else:
handle = doc.thumbnail_file
# TODO: Send ETag information and use that to send new thumbnails
# if available
return HttpResponse(handle,
content_type='image/png')
except (FileNotFoundError, Document.DoesNotExist):
@@ -283,16 +328,92 @@ class DocumentViewSet(RetrieveModelMixin,
raise Http404()
class LogViewSet(ReadOnlyModelViewSet):
model = Log
class SearchResultSerializer(DocumentSerializer):
def to_representation(self, instance):
doc = Document.objects.get(id=instance['id'])
r = super(SearchResultSerializer, self).to_representation(doc)
r['__search_hit__'] = {
"score": instance.score,
"highlights": instance.highlights("content",
text=doc.content) if doc else None, # NOQA: E501
"rank": instance.rank
}
return r
class UnifiedSearchViewSet(DocumentViewSet):
def __init__(self, *args, **kwargs):
super(UnifiedSearchViewSet, self).__init__(*args, **kwargs)
self.searcher = None
def get_serializer_class(self):
if self._is_search_request():
return SearchResultSerializer
else:
return DocumentSerializer
def _is_search_request(self):
return ("query" in self.request.query_params or
"more_like_id" in self.request.query_params)
def filter_queryset(self, queryset):
if self._is_search_request():
from documents import index
if "query" in self.request.query_params:
query_class = index.DelayedFullTextQuery
elif "more_like_id" in self.request.query_params:
query_class = index.DelayedMoreLikeThisQuery
else:
raise ValueError()
return query_class(
self.searcher,
self.request.query_params,
self.paginator.get_page_size(self.request))
else:
return super(UnifiedSearchViewSet, self).filter_queryset(queryset)
def list(self, request, *args, **kwargs):
if self._is_search_request():
from documents import index
try:
with index.open_index_searcher() as s:
self.searcher = s
return super(UnifiedSearchViewSet, self).list(request)
except NotFound:
raise
except Exception as e:
return HttpResponseBadRequest(str(e))
else:
return super(UnifiedSearchViewSet, self).list(request)
class LogViewSet(ViewSet):
queryset = Log.objects.all()
serializer_class = LogSerializer
pagination_class = StandardPagination
permission_classes = (IsAuthenticated,)
filter_backends = (DjangoFilterBackend, OrderingFilter)
filterset_class = LogFilterSet
ordering_fields = ("created",)
log_files = ["paperless", "mail"]
def retrieve(self, request, pk=None, *args, **kwargs):
if pk not in self.log_files:
raise Http404()
filename = os.path.join(settings.LOGGING_DIR, f"{pk}.log")
if not os.path.isfile(filename):
raise Http404()
with open(filename, "r") as f:
lines = [line.rstrip() for line in f.readlines()]
return Response(lines)
def list(self, request, *args, **kwargs):
return Response(self.log_files)
class SavedViewViewSet(ModelViewSet):
@@ -311,23 +432,12 @@ class SavedViewViewSet(ModelViewSet):
serializer.save(user=self.request.user)
class BulkEditView(APIView):
class BulkEditView(GenericAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = BulkEditSerializer
parser_classes = (parsers.JSONParser,)
def get_serializer_context(self):
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
def get_serializer(self, *args, **kwargs):
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
@@ -344,23 +454,12 @@ class BulkEditView(APIView):
return HttpResponseBadRequest(str(e))
class PostDocumentView(APIView):
class PostDocumentView(GenericAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = PostDocumentSerializer
parser_classes = (parsers.MultiPartParser,)
def get_serializer_context(self):
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
def get_serializer(self, *args, **kwargs):
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
@@ -381,35 +480,29 @@ class PostDocumentView(APIView):
delete=False) as f:
f.write(doc_data)
os.utime(f.name, times=(t, t))
temp_filename = f.name
task_id = str(uuid.uuid4())
async_task("documents.tasks.consume_file",
temp_filename,
override_filename=doc_name,
override_title=title,
override_correspondent_id=correspondent_id,
override_document_type_id=document_type_id,
override_tag_ids=tag_ids,
task_id=task_id,
task_name=os.path.basename(doc_name)[:100])
async_task("documents.tasks.consume_file",
f.name,
override_filename=doc_name,
override_title=title,
override_correspondent_id=correspondent_id,
override_document_type_id=document_type_id,
override_tag_ids=tag_ids,
task_name=os.path.basename(doc_name)[:100])
return Response("OK")
class SelectionDataView(APIView):
class SelectionDataView(GenericAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = SelectionDataSerializer
serializer_class = DocumentListSerializer
parser_classes = (parsers.MultiPartParser, parsers.JSONParser)
def get_serializer_context(self):
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
def get_serializer(self, *args, **kwargs):
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def post(self, request, format=None):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
@@ -450,83 +543,10 @@ class SelectionDataView(APIView):
return r
class SearchView(APIView):
permission_classes = (IsAuthenticated,)
def __init__(self, *args, **kwargs):
super(SearchView, self).__init__(*args, **kwargs)
self.ix = index.open_index()
def add_infos_to_hit(self, r):
try:
doc = Document.objects.get(id=r['id'])
except Document.DoesNotExist:
logging.getLogger(__name__).warning(
f"Search index returned a non-existing document: "
f"id: {r['id']}, title: {r['title']}. "
f"Search index needs reindex."
)
doc = None
return {'id': r['id'],
'highlights': r.highlights("content", text=doc.content) if doc else None, # NOQA: E501
'score': r.score,
'rank': r.rank,
'document': DocumentSerializer(doc).data if doc else None,
'title': r['title']
}
def get(self, request, format=None):
if 'query' in request.query_params:
query = request.query_params['query']
else:
query = None
if 'more_like' in request.query_params:
more_like_id = request.query_params['more_like']
more_like_content = Document.objects.get(id=more_like_id).content
else:
more_like_id = None
more_like_content = None
if not query and not more_like_id:
return Response({
'count': 0,
'page': 0,
'page_count': 0,
'corrected_query': None,
'results': []})
try:
page = int(request.query_params.get('page', 1))
except (ValueError, TypeError):
page = 1
if page < 1:
page = 1
try:
with index.query_page(self.ix, page, query, more_like_id, more_like_content) as (result_page, corrected_query): # NOQA: E501
return Response(
{'count': len(result_page),
'page': result_page.pagenum,
'page_count': result_page.pagecount,
'corrected_query': corrected_query,
'results': list(map(self.add_infos_to_hit, result_page))})
except Exception as e:
return HttpResponseBadRequest(str(e))
class SearchAutoCompleteView(APIView):
permission_classes = (IsAuthenticated,)
def __init__(self, *args, **kwargs):
super(SearchAutoCompleteView, self).__init__(*args, **kwargs)
self.ix = index.open_index()
def get(self, request, format=None):
if 'term' in request.query_params:
term = request.query_params['term']
@@ -540,7 +560,11 @@ class SearchAutoCompleteView(APIView):
else:
limit = 10
return Response(index.autocomplete(self.ix, term, limit))
from documents import index
ix = index.open_index()
return Response(index.autocomplete(ix, term, limit))
class StatisticsView(APIView):
@@ -548,8 +572,55 @@ class StatisticsView(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request, format=None):
return Response({
'documents_total': Document.objects.all().count(),
'documents_inbox': Document.objects.filter(
documents_total = Document.objects.all().count()
if Tag.objects.filter(is_inbox_tag=True).exists():
documents_inbox = Document.objects.filter(
tags__is_inbox_tag=True).distinct().count()
else:
documents_inbox = None
return Response({
'documents_total': documents_total,
'documents_inbox': documents_inbox,
})
class BulkDownloadView(GenericAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = BulkDownloadSerializer
parser_classes = (parsers.JSONParser,)
def post(self, request, format=None):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
ids = serializer.validated_data.get('documents')
compression = serializer.validated_data.get('compression')
content = serializer.validated_data.get('content')
os.makedirs(settings.SCRATCH_DIR, exist_ok=True)
temp = tempfile.NamedTemporaryFile(
dir=settings.SCRATCH_DIR,
suffix="-compressed-archive",
delete=False)
if content == 'both':
strategy_class = OriginalAndArchiveStrategy
elif content == 'originals':
strategy_class = OriginalsOnlyStrategy
else:
strategy_class = ArchiveOnlyStrategy
with zipfile.ZipFile(temp.name, "w", compression) as zipf:
strategy = strategy_class(zipf)
for id in ids:
doc = Document.objects.get(id=id)
strategy.add_document(doc)
with open(temp.name, "rb") as f:
response = HttpResponse(f, content_type="application/zip")
response["Content-Disposition"] = '{}; filename="{}"'.format(
"attachment", "documents.zip")
return response

View File

@@ -0,0 +1,698 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-05-16 10:09\n"
"Last-Translator: \n"
"Language-Team: Czech\n"
"Language: cs_CZ\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==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
"X-Crowdin-Project: paperless-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: cs\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Dokumenty"
#: documents/models.py:32
msgid "Any word"
msgstr "Jakékoliv slovo"
#: documents/models.py:33
msgid "All words"
msgstr "Všechna slova"
#: documents/models.py:34
msgid "Exact match"
msgstr "Přesná shoda"
#: documents/models.py:35
msgid "Regular expression"
msgstr "Regulární výraz"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Fuzzy slovo"
#: documents/models.py:37
msgid "Automatic"
msgstr "Automatický"
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "název"
#: documents/models.py:45
msgid "match"
msgstr "shoda"
#: documents/models.py:49
msgid "matching algorithm"
msgstr "algoritmus pro shodu"
#: documents/models.py:55
msgid "is insensitive"
msgstr "je ignorováno"
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "korespondent"
#: documents/models.py:75
msgid "correspondents"
msgstr "korespondenti"
#: documents/models.py:81
msgid "color"
msgstr "barva"
#: documents/models.py:87
msgid "is inbox tag"
msgstr "tag přichozí"
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Označí tento tag jako tag pro příchozí: Všechny nově zkonzumované dokumenty budou označeny tagem pro přichozí"
#: documents/models.py:94
msgid "tag"
msgstr "tag"
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "tagy"
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "typ dokumentu"
#: documents/models.py:102
msgid "document types"
msgstr "typy dokumentu"
#: documents/models.py:110
msgid "Unencrypted"
msgstr "Nešifrované"
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "Šifrované pomocí GNU Privacy Guard"
#: documents/models.py:124
msgid "title"
msgstr "titulek"
#: documents/models.py:137
msgid "content"
msgstr "obsah"
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "Nezpracovaná, pouze textová data dokumentu. Toto pole je používáno především pro vyhledávání."
#: documents/models.py:144
msgid "mime type"
msgstr "mime typ"
#: documents/models.py:155
msgid "checksum"
msgstr "kontrolní součet"
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "Kontrolní součet původního dokumentu"
#: documents/models.py:163
msgid "archive checksum"
msgstr "kontrolní součet archivu"
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "Kontrolní součet archivovaného dokumentu."
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "vytvořeno"
#: documents/models.py:176
msgid "modified"
msgstr "upraveno"
#: documents/models.py:180
msgid "storage type"
msgstr "typ úložiště"
#: documents/models.py:188
msgid "added"
msgstr "přidáno"
#: documents/models.py:192
msgid "filename"
msgstr "název souboru"
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "Aktuální název souboru v úložišti"
#: documents/models.py:202
msgid "archive filename"
msgstr ""
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr ""
#: documents/models.py:212
msgid "archive serial number"
msgstr "sériové číslo archivu"
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr "Pozice dokumentu ve vašem archivu fyzických dokumentů"
#: documents/models.py:223
msgid "document"
msgstr "dokument"
#: documents/models.py:224
msgid "documents"
msgstr "dokumenty"
#: documents/models.py:311
msgid "debug"
msgstr "debug"
#: documents/models.py:312
msgid "information"
msgstr "informace"
#: documents/models.py:313
msgid "warning"
msgstr "varování"
#: documents/models.py:314
msgid "error"
msgstr "chyba"
#: documents/models.py:315
msgid "critical"
msgstr "kritická"
#: documents/models.py:319
msgid "group"
msgstr "skupina"
#: documents/models.py:322
msgid "message"
msgstr "zpráva"
#: documents/models.py:325
msgid "level"
msgstr "úroveň"
#: documents/models.py:332
msgid "log"
msgstr "záznam"
#: documents/models.py:333
msgid "logs"
msgstr "záznamy"
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "uložený pohled"
#: documents/models.py:345
msgid "saved views"
msgstr "uložené pohledy"
#: documents/models.py:348
msgid "user"
msgstr "uživatel"
#: documents/models.py:354
msgid "show on dashboard"
msgstr "zobrazit v dashboardu"
#: documents/models.py:357
msgid "show in sidebar"
msgstr "zobrazit v postranním menu"
#: documents/models.py:361
msgid "sort field"
msgstr "pole na řazení"
#: documents/models.py:367
msgid "sort reverse"
msgstr "třídit opačně"
#: documents/models.py:373
msgid "title contains"
msgstr "titulek obsahuje"
#: documents/models.py:374
msgid "content contains"
msgstr "obsah obsahuje"
#: documents/models.py:375
msgid "ASN is"
msgstr "ASN je"
#: documents/models.py:376
msgid "correspondent is"
msgstr "korespondent je"
#: documents/models.py:377
msgid "document type is"
msgstr "typ dokumentu je"
#: documents/models.py:378
msgid "is in inbox"
msgstr "je v příchozích"
#: documents/models.py:379
msgid "has tag"
msgstr "má tag"
#: documents/models.py:380
msgid "has any tag"
msgstr "má jakýkoliv tag"
#: documents/models.py:381
msgid "created before"
msgstr "vytvořeno před"
#: documents/models.py:382
msgid "created after"
msgstr "vytvořeno po"
#: documents/models.py:383
msgid "created year is"
msgstr "rok vytvoření je"
#: documents/models.py:384
msgid "created month is"
msgstr "měsíc vytvoření je"
#: documents/models.py:385
msgid "created day is"
msgstr "den vytvoření je"
#: documents/models.py:386
msgid "added before"
msgstr "přidáno před"
#: documents/models.py:387
msgid "added after"
msgstr "přidáno po"
#: documents/models.py:388
msgid "modified before"
msgstr "upraveno před"
#: documents/models.py:389
msgid "modified after"
msgstr "upraveno po"
#: documents/models.py:390
msgid "does not have tag"
msgstr "nemá tag"
#: documents/models.py:391
msgid "does not have ASN"
msgstr ""
#: documents/models.py:392
msgid "title or content contains"
msgstr ""
#: documents/models.py:393
msgid "fulltext query"
msgstr ""
#: documents/models.py:394
msgid "more like this"
msgstr ""
#: documents/models.py:405
msgid "rule type"
msgstr "typ pravidla"
#: documents/models.py:409
msgid "value"
msgstr "hodnota"
#: documents/models.py:415
msgid "filter rule"
msgstr "filtrovací pravidlo"
#: documents/models.py:416
msgid "filter rules"
msgstr "filtrovací pravidla"
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr ""
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr ""
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "Typ souboru %(type)s není podporován"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "Paperless-ng se načítá..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "Odhlášeno od Paperless-ng"
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "Byli jste úspěšně odhlášeni. Nashledanou!"
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "Přihlašte se znovu"
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "Paperless-ng přihlášení"
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "Prosím přihlaste se."
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr "Vaše uživatelské jméno a heslo se neshodují. Prosím, zkuste to znovu."
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "Uživatelské jméno"
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "Heslo"
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "Přihlásit se"
#: paperless/settings.py:303
msgid "English (US)"
msgstr ""
#: paperless/settings.py:304
msgid "English (GB)"
msgstr ""
#: paperless/settings.py:305
msgid "German"
msgstr "Němčina"
#: paperless/settings.py:306
msgid "Dutch"
msgstr "Holandština"
#: paperless/settings.py:307
msgid "French"
msgstr "Francouzština"
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr ""
#: paperless/settings.py:309
msgid "Portuguese"
msgstr ""
#: paperless/settings.py:310
msgid "Italian"
msgstr ""
#: paperless/settings.py:311
msgid "Romanian"
msgstr ""
#: paperless/settings.py:312
msgid "Russian"
msgstr ""
#: paperless/settings.py:313
msgid "Spanish"
msgstr ""
#: paperless/settings.py:314
msgid "Polish"
msgstr ""
#: paperless/settings.py:315
msgid "Swedish"
msgstr ""
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "Správa Paperless-ng"
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr ""
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr ""
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Filtr"
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless zpracuje pouze emaily které odpovídají VŠEM níže zadaným filtrům."
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Akce"
#: paperless_mail/admin.py:51
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 "Akce provedena na emailu. Tato akce je provedena jen pokud byly dokumenty zkonzumovány z emailu. Emaily bez příloh zůstanou nedotčeny."
#: paperless_mail/admin.py:58
msgid "Metadata"
msgstr "Metadata"
#: paperless_mail/admin.py:60
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 "Automaticky přiřadit metadata dokumentům zkonzumovaných z tohoto pravidla. Pokud zde nepřiřadíte tagy, typy nebo korespondenty, paperless stále zpracuje všechna shodující-se pravidla které jste definovali."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Paperless pošta"
#: paperless_mail/models.py:11
msgid "mail account"
msgstr "emailový účet"
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr "emailové účty"
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr "Žádné šifrování"
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr "Používat SSL"
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr "Používat STARTTLS"
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr "IMAP server"
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr "IMAP port"
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "Toto je většinou 143 pro nešifrovaná připojení/připojení používající STARTTLS a 993 pro SSL připojení."
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr "IMAP bezpečnost"
#: paperless_mail/models.py:46
msgid "username"
msgstr "uživatelské jméno"
#: paperless_mail/models.py:50
msgid "password"
msgstr "heslo"
#: paperless_mail/models.py:54
msgid "character set"
msgstr ""
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr ""
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "mailové pravidlo"
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "mailová pravidla"
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "Zpracovávat jen přílohy"
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "Zpracovat všechny soubory, včetně vložených příloh"
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "Označit jako přečtené, nezpracovávat přečtené emaily"
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "Označit email, nezpracovávat označené emaily"
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "Přesunout do specifikované složky"
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Odstranit"
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "Použít předmět jako titulek"
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "Použít název souboru u přílohy jako titulek"
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "Nepřiřazovat korespondenta"
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "Použít emailovou adresu"
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "Použít jméno (nebo emailovou adresu pokud jméno není dostupné)"
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "Použít korespondenta vybraného níže"
#: paperless_mail/models.py:121
msgid "order"
msgstr "pořadí"
#: paperless_mail/models.py:128
msgid "account"
msgstr "účet"
#: paperless_mail/models.py:132
msgid "folder"
msgstr "složka"
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr ""
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "filtrovat z"
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "název filtru"
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "tělo filtru"
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "název souboru u přílohy filtru"
#: paperless_mail/models.py:150
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "Konzumovat jen dokumenty které přesně odpovídají tomuto názvu souboru pokud specifikováno. Zástupné znaky jako *.pdf nebo *invoice* jsou povoleny. Nezáleží na velikosti písmen."
#: paperless_mail/models.py:156
msgid "maximum age"
msgstr "maximální stáří"
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "Specifikováno ve dnech."
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "typ přílohy"
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "Vložené přílohy zahrnují vložené obrázky, takže je nejlepší tuto možnost kombinovat s filtrem na název souboru"
#: paperless_mail/models.py:169
msgid "action"
msgstr "akce"
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "parametr akce"
#: paperless_mail/models.py:177
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:184
msgid "assign title from"
msgstr "nastavit titulek z"
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "přiřadit tento tag"
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "přiřadit tento typ dokumentu"
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "přiřadit korespondenta z"
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "přiřadit tohoto korespondenta"

View File

@@ -1,25 +1,21 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jonas Winkler, 2021
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-10 21:41+0000\n"
"PO-Revision-Date: 2020-12-30 19:27+0000\n"
"Last-Translator: Jonas Winkler, 2021\n"
"Language-Team: German (https://www.transifex.com/paperless/teams/115905/de/)\n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-07-05 11:17\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: paperless-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: de\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
@@ -39,7 +35,7 @@ msgstr "Exakte Übereinstimmung"
#: documents/models.py:35
msgid "Regular expression"
msgstr "Regulärer Ausdruck"
msgstr "Regular expression / Reguläre Ausdrücke"
#: documents/models.py:36
msgid "Fuzzy word"
@@ -49,8 +45,8 @@ msgstr "Ungenaues Wort"
msgid "Automatic"
msgstr "Automatisch"
#: documents/models.py:41 documents/models.py:354 paperless_mail/models.py:25
#: paperless_mail/models.py:109
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "Name"
@@ -66,387 +62,443 @@ msgstr "Zuweisungsalgorithmus"
msgid "is insensitive"
msgstr "Groß-/Kleinschreibung irrelevant"
#: documents/models.py:80 documents/models.py:140
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "Korrespondent"
#: documents/models.py:81
#: documents/models.py:75
msgid "correspondents"
msgstr "Korrespondenten"
#: documents/models.py:103
#: documents/models.py:81
msgid "color"
msgstr "Farbe"
#: documents/models.py:107
#: documents/models.py:87
msgid "is inbox tag"
msgstr "Posteingangs-Tag"
#: documents/models.py:109
msgid ""
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
"with inbox tags."
msgstr ""
"Markiert das Tag als Posteingangs-Tag. Neue Dokumente werden immer mit "
"diesem Tag versehen."
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Markiert das Tag als Posteingangs-Tag. Neue Dokumente werden immer mit diesem Tag versehen."
#: documents/models.py:114
#: documents/models.py:94
msgid "tag"
msgstr "Tag"
#: documents/models.py:115 documents/models.py:171
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "Tags"
#: documents/models.py:121 documents/models.py:153
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "Dokumenttyp"
#: documents/models.py:122
#: documents/models.py:102
msgid "document types"
msgstr "Dokumenttypen"
#: documents/models.py:130
#: documents/models.py:110
msgid "Unencrypted"
msgstr "Nicht verschlüsselt"
#: documents/models.py:131
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "Verschlüsselt mit GNU Privacy Guard"
#: documents/models.py:144
#: documents/models.py:124
msgid "title"
msgstr "Titel"
#: documents/models.py:157
#: documents/models.py:137
msgid "content"
msgstr "Inhalt"
#: documents/models.py:159
msgid ""
"The raw, text-only data of the document. This field is primarily used for "
"searching."
msgstr ""
"Der Inhalt des Dokuments in Textform. Dieses Feld wird primär für die Suche "
"verwendet."
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "Der Inhalt des Dokuments in Textform. Dieses Feld wird primär für die Suche verwendet."
#: documents/models.py:164
#: documents/models.py:144
msgid "mime type"
msgstr "MIME-Typ"
#: documents/models.py:175
#: documents/models.py:155
msgid "checksum"
msgstr "Prüfsumme"
#: documents/models.py:179
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "Die Prüfsumme des originalen Dokuments."
#: documents/models.py:183
#: documents/models.py:163
msgid "archive checksum"
msgstr "Archiv-Prüfsumme"
#: documents/models.py:188
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "Die Prüfsumme des archivierten Dokuments."
#: documents/models.py:192 documents/models.py:332
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "Erstellt"
msgstr "Ausgestellt"
#: documents/models.py:196
#: documents/models.py:176
msgid "modified"
msgstr "Geändert"
#: documents/models.py:200
#: documents/models.py:180
msgid "storage type"
msgstr "Speichertyp"
#: documents/models.py:208
#: documents/models.py:188
msgid "added"
msgstr "Hinzugefügt"
#: documents/models.py:212
#: documents/models.py:192
msgid "filename"
msgstr "Dateiname"
#: documents/models.py:217
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "Aktueller Dateiname im Datenspeicher"
#: documents/models.py:221
#: documents/models.py:202
msgid "archive filename"
msgstr "Archiv-Dateiname"
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr "Aktueller Dateiname im Archiv"
#: documents/models.py:212
msgid "archive serial number"
msgstr "Archiv-Seriennummer"
#: documents/models.py:226
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr "Die Position dieses Dokuments in Ihrem physischen Dokumentenarchiv."
#: documents/models.py:232
#: documents/models.py:223
msgid "document"
msgstr "Dokument"
#: documents/models.py:233
#: documents/models.py:224
msgid "documents"
msgstr "Dokumente"
#: documents/models.py:315
#: documents/models.py:311
msgid "debug"
msgstr "Debug"
#: documents/models.py:316
#: documents/models.py:312
msgid "information"
msgstr "Information"
#: documents/models.py:317
#: documents/models.py:313
msgid "warning"
msgstr "Warnung"
#: documents/models.py:318
#: documents/models.py:314
msgid "error"
msgstr "Fehler"
#: documents/models.py:319
#: documents/models.py:315
msgid "critical"
msgstr "Kritisch"
#: documents/models.py:323
#: documents/models.py:319
msgid "group"
msgstr "Gruppe"
#: documents/models.py:326
#: documents/models.py:322
msgid "message"
msgstr "Nachricht"
#: documents/models.py:329
#: documents/models.py:325
msgid "level"
msgstr "Level"
#: documents/models.py:336
#: documents/models.py:332
msgid "log"
msgstr "Protokoll"
#: documents/models.py:337
#: documents/models.py:333
msgid "logs"
msgstr "Protokoll"
#: documents/models.py:348 documents/models.py:398
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "Gespeicherte Ansicht"
#: documents/models.py:349
#: documents/models.py:345
msgid "saved views"
msgstr "Gespeicherte Ansichten"
#: documents/models.py:352
#: documents/models.py:348
msgid "user"
msgstr "Benutzer"
#: documents/models.py:358
#: documents/models.py:354
msgid "show on dashboard"
msgstr "Auf Startseite zeigen"
#: documents/models.py:361
#: documents/models.py:357
msgid "show in sidebar"
msgstr "In Seitenleiste zeigen"
#: documents/models.py:365
#: documents/models.py:361
msgid "sort field"
msgstr "Sortierfeld"
#: documents/models.py:368
#: documents/models.py:367
msgid "sort reverse"
msgstr "Umgekehrte Sortierung"
#: documents/models.py:374
#: documents/models.py:373
msgid "title contains"
msgstr "Titel enthält"
#: documents/models.py:375
#: documents/models.py:374
msgid "content contains"
msgstr "Inhalt enthält"
#: documents/models.py:376
#: documents/models.py:375
msgid "ASN is"
msgstr "ASN ist"
#: documents/models.py:377
#: documents/models.py:376
msgid "correspondent is"
msgstr "Korrespondent ist"
#: documents/models.py:378
#: documents/models.py:377
msgid "document type is"
msgstr "Dokumenttyp ist"
#: documents/models.py:379
#: documents/models.py:378
msgid "is in inbox"
msgstr "Ist im Posteingang"
#: documents/models.py:380
#: documents/models.py:379
msgid "has tag"
msgstr "Hat Tag"
#: documents/models.py:381
#: documents/models.py:380
msgid "has any tag"
msgstr "Hat irgendein Tag"
#: documents/models.py:382
#: documents/models.py:381
msgid "created before"
msgstr "Erstellt vor"
msgstr "Ausgestellt vor"
#: documents/models.py:382
msgid "created after"
msgstr "Ausgestellt nach"
#: documents/models.py:383
msgid "created after"
msgstr "Erstellt nach"
msgid "created year is"
msgstr "Ausgestellt im Jahr"
#: documents/models.py:384
msgid "created year is"
msgstr "Erstellt im Jahr"
msgid "created month is"
msgstr "Ausgestellt im Monat"
#: documents/models.py:385
msgid "created month is"
msgstr "Erstellt im Monat"
msgid "created day is"
msgstr "Ausgestellt am Tag"
#: documents/models.py:386
msgid "created day is"
msgstr "Erstellt am Tag"
#: documents/models.py:387
msgid "added before"
msgstr "Hinzugefügt vor"
#: documents/models.py:388
#: documents/models.py:387
msgid "added after"
msgstr "Hinzugefügt nach"
#: documents/models.py:389
#: documents/models.py:388
msgid "modified before"
msgstr "Geändert vor"
#: documents/models.py:390
#: documents/models.py:389
msgid "modified after"
msgstr "Geändert nach"
#: documents/models.py:391
#: documents/models.py:390
msgid "does not have tag"
msgstr "Hat nicht folgendes Tag"
#: documents/models.py:402
#: documents/models.py:391
msgid "does not have ASN"
msgstr "Dokument hat keine ASN"
#: documents/models.py:392
msgid "title or content contains"
msgstr "Titel oder Inhalt enthält"
#: documents/models.py:393
msgid "fulltext query"
msgstr "Volltextsuche"
#: documents/models.py:394
msgid "more like this"
msgstr "Ähnliche Dokumente"
#: documents/models.py:405
msgid "rule type"
msgstr "Regeltyp"
#: documents/models.py:406
#: documents/models.py:409
msgid "value"
msgstr "Wert"
#: documents/models.py:412
#: documents/models.py:415
msgid "filter rule"
msgstr "Filterregel"
#: documents/models.py:413
#: documents/models.py:416
msgid "filter rules"
msgstr "Filterregeln"
#: documents/templates/index.html:20
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "Ungültiger regulärer Ausdruck: %(error)s"
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr "Ungültige Farbe."
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "Dateityp %(type)s nicht unterstützt"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "Paperless-ng wird geladen..."
#: documents/templates/registration/logged_out.html:13
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "Paperless-ng abgemeldet"
#: documents/templates/registration/logged_out.html:41
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "Sie wurden erfolgreich abgemeldet. Auf Wiedersehen!"
#: documents/templates/registration/logged_out.html:42
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "Erneut anmelden"
#: documents/templates/registration/login.html:13
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "Paperless-ng Anmeldung"
#: documents/templates/registration/login.html:42
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "Bitte melden Sie sich an."
#: documents/templates/registration/login.html:45
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr ""
"Ihr Benutzername und Passwort stimmen nicht überein. Bitte versuchen Sie es "
"erneut."
msgstr "Ihr Benutzername und Kennwort stimmen nicht überein. Bitte versuchen Sie es erneut."
#: documents/templates/registration/login.html:48
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "Benutzername"
#: documents/templates/registration/login.html:49
msgid "Password"
msgstr "Passwort"
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "Kennwort"
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "Anmelden"
#: paperless/settings.py:268
msgid "English"
msgstr "Englisch"
#: paperless/settings.py:303
msgid "English (US)"
msgstr "Englisch (US)"
#: paperless/settings.py:269
#: paperless/settings.py:304
msgid "English (GB)"
msgstr "Englisch (UK)"
#: paperless/settings.py:305
msgid "German"
msgstr "Deutsch"
#: paperless/settings.py:270
#: paperless/settings.py:306
msgid "Dutch"
msgstr "Niederländisch"
#: paperless/settings.py:271
#: paperless/settings.py:307
msgid "French"
msgstr "Französisch"
#: paperless/urls.py:108
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr "Portugiesisch (Brasilien)"
#: paperless/settings.py:309
msgid "Portuguese"
msgstr "Portugiesisch"
#: paperless/settings.py:310
msgid "Italian"
msgstr "Italienisch"
#: paperless/settings.py:311
msgid "Romanian"
msgstr "Rumänisch"
#: paperless/settings.py:312
msgid "Russian"
msgstr "Russisch"
#: paperless/settings.py:313
msgid "Spanish"
msgstr "Spanisch"
#: paperless/settings.py:314
msgid "Polish"
msgstr "Polnisch"
#: paperless/settings.py:315
msgid "Swedish"
msgstr "Schwedisch"
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "Paperless-ng Administration"
#: paperless_mail/admin.py:25
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr "Authentifizierung"
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr "Erweiterte Einstellungen"
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Filter"
#: paperless_mail/admin.py:27
msgid ""
"Paperless will only process mails that match ALL of the filters given below."
msgstr ""
"Paperless wird nur E-Mails verarbeiten, für die alle der hier angegebenen "
"Filter zutreffen."
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless wird nur E-Mails verarbeiten, für die alle der hier angegebenen Filter zutreffen."
#: paperless_mail/admin.py:37
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Aktionen"
#: paperless_mail/admin.py:39
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 ""
"Die Aktion, die auf E-Mails angewendet werden soll. Diese Aktion wird nur "
"auf E-Mails angewendet, aus denen Anhänge verarbeitet wurden. E-Mails ohne "
"Anhänge werden vollständig ignoriert."
#: paperless_mail/admin.py:51
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 "Die Aktion, die auf E-Mails angewendet werden soll. Diese Aktion wird nur auf E-Mails angewendet, aus denen Anhänge verarbeitet wurden. E-Mails ohne Anhänge werden vollständig ignoriert."
#: paperless_mail/admin.py:46
#: paperless_mail/admin.py:58
msgid "Metadata"
msgstr "Metadaten"
#: paperless_mail/admin.py:48
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 ""
"Folgende Metadaten werden Dokumenten dieser Regel automatisch zugewiesen. "
"Wenn Sie hier nichts auswählen wird Paperless weiterhin alle "
"Zuweisungsalgorithmen ausführen und Metadaten auf Basis des Dokumentinhalts "
"zuweisen."
#: paperless_mail/admin.py:60
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 "Folgende Metadaten werden Dokumenten dieser Regel automatisch zugewiesen. Wenn Sie hier nichts auswählen wird Paperless weiterhin alle Zuweisungsalgorithmen ausführen und Metadaten auf Basis des Dokumentinhalts zuweisen."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
@@ -481,12 +533,8 @@ msgid "IMAP port"
msgstr "IMAP-Port"
#: paperless_mail/models.py:36
msgid ""
"This is usually 143 for unencrypted and STARTTLS connections, and 993 for "
"SSL connections."
msgstr ""
"Dies ist in der Regel 143 für unverschlüsselte und STARTTLS-Verbindungen und"
" 993 für SSL-Verbindungen."
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "Dies ist in der Regel 143 für unverschlüsselte und STARTTLS-Verbindungen und 993 für SSL-Verbindungen."
#: paperless_mail/models.py:40
msgid "IMAP security"
@@ -498,153 +546,153 @@ msgstr "Benutzername"
#: paperless_mail/models.py:50
msgid "password"
msgstr "Passwort"
msgstr "Kennwort"
#: paperless_mail/models.py:60
#: paperless_mail/models.py:54
msgid "character set"
msgstr "Zeichensatz"
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr "Der Zeichensatz, der bei der Kommunikation mit dem Mailserver verwendet werden soll, wie z.B. 'UTF-8' oder 'US-ASCII'."
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "E-Mail-Regel"
#: paperless_mail/models.py:61
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "E-Mail-Regeln"
#: paperless_mail/models.py:67
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "Nur Anhänge verarbeiten."
#: paperless_mail/models.py:68
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "Alle Dateien verarbeiten, auch 'inline'-Anhänge."
#: paperless_mail/models.py:78
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "Als gelesen markieren, gelesene E-Mails nicht verarbeiten"
#: paperless_mail/models.py:79
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "Als wichtig markieren, markierte E-Mails nicht verarbeiten"
#: paperless_mail/models.py:80
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "In angegebenen Ordner verschieben"
#: paperless_mail/models.py:81
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Löschen"
#: paperless_mail/models.py:88
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "Betreff als Titel verwenden"
#: paperless_mail/models.py:89
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "Dateiname des Anhangs als Titel verwenden"
#: paperless_mail/models.py:99
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "Keinen Korrespondenten zuweisen"
#: paperless_mail/models.py:101
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "E-Mail-Adresse benutzen"
#: paperless_mail/models.py:103
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "Absendername benutzen (oder E-Mail-Adressen, wenn nicht verfügbar)"
#: paperless_mail/models.py:105
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "Nachfolgend ausgewählten Korrespondent verwenden"
#: paperless_mail/models.py:113
#: paperless_mail/models.py:121
msgid "order"
msgstr "Reihenfolge"
#: paperless_mail/models.py:120
#: paperless_mail/models.py:128
msgid "account"
msgstr "Konto"
#: paperless_mail/models.py:124
#: paperless_mail/models.py:132
msgid "folder"
msgstr "Ordner"
#: paperless_mail/models.py:128
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr "Unterordner müssen durch Punkte getrennt werden."
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "Absender filtern"
#: paperless_mail/models.py:131
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "Betreff filtern"
#: paperless_mail/models.py:134
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "Nachrichteninhalt filtern"
#: paperless_mail/models.py:138
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "Anhang-Dateiname filtern"
#: paperless_mail/models.py:140
msgid ""
"Only consume documents which entirely match this filename if specified. "
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr ""
"Wenn angegeben werden nur Dateien verarbeitet, die diesem Dateinamen exakt "
"entsprechen. Platzhalter wie *.pdf oder *rechnung* sind erlaubt. Groß- und "
"Kleinschreibung ist irrelevant."
#: paperless_mail/models.py:150
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "Wenn angegeben werden nur Dateien verarbeitet, die diesem Dateinamen exakt entsprechen. Platzhalter wie *.pdf oder *rechnung* sind erlaubt. Groß- und Kleinschreibung ist irrelevant."
#: paperless_mail/models.py:146
#: paperless_mail/models.py:156
msgid "maximum age"
msgstr "Maximales Alter"
#: paperless_mail/models.py:148
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "Angegeben in Tagen."
#: paperless_mail/models.py:151
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "Dateianhangstyp"
#: paperless_mail/models.py:154
msgid ""
"Inline attachments include embedded images, so it's best to combine this "
"option with a filename filter."
msgstr ""
"'Inline'-Anhänge schließen eingebettete Bilder mit ein, daher sollte diese "
"Einstellung mit einem Dateinamenfilter kombiniert werden."
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "'Inline'-Anhänge schließen eingebettete Bilder mit ein, daher sollte diese Einstellung mit einem Dateinamenfilter kombiniert werden."
#: paperless_mail/models.py:159
#: paperless_mail/models.py:169
msgid "action"
msgstr "Aktion"
#: paperless_mail/models.py:165
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "Parameter für Aktion"
#: paperless_mail/models.py:167
msgid ""
"Additional parameter for the action selected above, i.e., the target folder "
"of the move to folder action."
msgstr ""
"Zusätzlicher Parameter für die oben ausgewählte Aktion, zum Beispiel der "
"Zielordner für die Aktion \"In angegebenen Ordner verschieben\""
#: paperless_mail/models.py:177
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 "Zusätzlicher Parameter für die oben ausgewählte Aktion, zum Beispiel der Zielordner für die Aktion \"In angegebenen Ordner verschieben\". Unterordner müssen durch Punkte getrennt werden."
#: paperless_mail/models.py:173
#: paperless_mail/models.py:184
msgid "assign title from"
msgstr "Titel zuweisen von"
#: paperless_mail/models.py:183
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "Dieses Tag zuweisen"
#: paperless_mail/models.py:191
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "Diesen Dokumenttyp zuweisen"
#: paperless_mail/models.py:195
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "Korrespondent zuweisen von"
#: paperless_mail/models.py:205
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "Diesen Korrespondent zuweisen"

View File

@@ -0,0 +1,698 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-06-19 21:20\n"
"Last-Translator: \n"
"Language-Team: English, United Kingdom\n"
"Language: en_GB\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-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: en-GB\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Documents"
#: documents/models.py:32
msgid "Any word"
msgstr "Any word"
#: documents/models.py:33
msgid "All words"
msgstr "All words"
#: documents/models.py:34
msgid "Exact match"
msgstr "Exact match"
#: documents/models.py:35
msgid "Regular expression"
msgstr "Regular expression"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Fuzzy word"
#: documents/models.py:37
msgid "Automatic"
msgstr "Automatic"
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "name"
#: documents/models.py:45
msgid "match"
msgstr "match"
#: documents/models.py:49
msgid "matching algorithm"
msgstr "matching algorithm"
#: documents/models.py:55
msgid "is insensitive"
msgstr "is insensitive"
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "correspondent"
#: documents/models.py:75
msgid "correspondents"
msgstr "correspondents"
#: documents/models.py:81
msgid "color"
msgstr "colour"
#: documents/models.py:87
msgid "is inbox tag"
msgstr "is inbox tag"
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
#: documents/models.py:94
msgid "tag"
msgstr "tag"
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "tags"
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "document type"
#: documents/models.py:102
msgid "document types"
msgstr "document types"
#: documents/models.py:110
msgid "Unencrypted"
msgstr "Unencrypted"
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "Encrypted with GNU Privacy Guard"
#: documents/models.py:124
msgid "title"
msgstr "title"
#: documents/models.py:137
msgid "content"
msgstr "content"
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "The raw, text-only data of the document. This field is primarily used for searching."
#: documents/models.py:144
msgid "mime type"
msgstr "mime type"
#: documents/models.py:155
msgid "checksum"
msgstr "checksum"
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "The checksum of the original document."
#: documents/models.py:163
msgid "archive checksum"
msgstr "archive checksum"
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "The checksum of the archived document."
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "created"
#: documents/models.py:176
msgid "modified"
msgstr "modified"
#: documents/models.py:180
msgid "storage type"
msgstr "storage type"
#: documents/models.py:188
msgid "added"
msgstr "added"
#: documents/models.py:192
msgid "filename"
msgstr "filename"
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "Current filename in storage"
#: documents/models.py:202
msgid "archive filename"
msgstr "archive filename"
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr "Current archive filename in storage"
#: documents/models.py:212
msgid "archive serial number"
msgstr "archive serial number"
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr "The position of this document in your physical document archive."
#: documents/models.py:223
msgid "document"
msgstr "document"
#: documents/models.py:224
msgid "documents"
msgstr "documents"
#: documents/models.py:311
msgid "debug"
msgstr "debug"
#: documents/models.py:312
msgid "information"
msgstr "information"
#: documents/models.py:313
msgid "warning"
msgstr "warning"
#: documents/models.py:314
msgid "error"
msgstr "error"
#: documents/models.py:315
msgid "critical"
msgstr "critical"
#: documents/models.py:319
msgid "group"
msgstr "group"
#: documents/models.py:322
msgid "message"
msgstr "message"
#: documents/models.py:325
msgid "level"
msgstr "level"
#: documents/models.py:332
msgid "log"
msgstr "log"
#: documents/models.py:333
msgid "logs"
msgstr "logs"
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "saved view"
#: documents/models.py:345
msgid "saved views"
msgstr "saved views"
#: documents/models.py:348
msgid "user"
msgstr "user"
#: documents/models.py:354
msgid "show on dashboard"
msgstr "show on dashboard"
#: documents/models.py:357
msgid "show in sidebar"
msgstr "show in sidebar"
#: documents/models.py:361
msgid "sort field"
msgstr "sort field"
#: documents/models.py:367
msgid "sort reverse"
msgstr "sort reverse"
#: documents/models.py:373
msgid "title contains"
msgstr "title contains"
#: documents/models.py:374
msgid "content contains"
msgstr "content contains"
#: documents/models.py:375
msgid "ASN is"
msgstr "ASN is"
#: documents/models.py:376
msgid "correspondent is"
msgstr "correspondent is"
#: documents/models.py:377
msgid "document type is"
msgstr "document type is"
#: documents/models.py:378
msgid "is in inbox"
msgstr "is in inbox"
#: documents/models.py:379
msgid "has tag"
msgstr "has tag"
#: documents/models.py:380
msgid "has any tag"
msgstr "has any tag"
#: documents/models.py:381
msgid "created before"
msgstr "created before"
#: documents/models.py:382
msgid "created after"
msgstr "created after"
#: documents/models.py:383
msgid "created year is"
msgstr "created year is"
#: documents/models.py:384
msgid "created month is"
msgstr "created month is"
#: documents/models.py:385
msgid "created day is"
msgstr "created day is"
#: documents/models.py:386
msgid "added before"
msgstr "added before"
#: documents/models.py:387
msgid "added after"
msgstr "added after"
#: documents/models.py:388
msgid "modified before"
msgstr "modified before"
#: documents/models.py:389
msgid "modified after"
msgstr "modified after"
#: documents/models.py:390
msgid "does not have tag"
msgstr "does not have tag"
#: documents/models.py:391
msgid "does not have ASN"
msgstr "does not have ASN"
#: documents/models.py:392
msgid "title or content contains"
msgstr "title or content contains"
#: documents/models.py:393
msgid "fulltext query"
msgstr "fulltext query"
#: documents/models.py:394
msgid "more like this"
msgstr "more like this"
#: documents/models.py:405
msgid "rule type"
msgstr "rule type"
#: documents/models.py:409
msgid "value"
msgstr "value"
#: documents/models.py:415
msgid "filter rule"
msgstr "filter rule"
#: documents/models.py:416
msgid "filter rules"
msgstr "filter rules"
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "Invalid regular expression: %(error)s"
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr "Invalid colour."
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "File type %(type)s not supported"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "Paperless-ng is loading..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "Paperless-ng signed out"
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "You have been successfully logged out. Bye!"
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "Sign in again"
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "Paperless-ng sign in"
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "Please sign in."
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr "Your username and password didn't match. Please try again."
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "Username"
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "Password"
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "Sign in"
#: paperless/settings.py:303
msgid "English (US)"
msgstr "English (US)"
#: paperless/settings.py:304
msgid "English (GB)"
msgstr "English (GB)"
#: paperless/settings.py:305
msgid "German"
msgstr "German"
#: paperless/settings.py:306
msgid "Dutch"
msgstr "Dutch"
#: paperless/settings.py:307
msgid "French"
msgstr "French"
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr "Portuguese (Brazil)"
#: paperless/settings.py:309
msgid "Portuguese"
msgstr "Portuguese"
#: paperless/settings.py:310
msgid "Italian"
msgstr "Italian"
#: paperless/settings.py:311
msgid "Romanian"
msgstr "Romanian"
#: paperless/settings.py:312
msgid "Russian"
msgstr "Russian"
#: paperless/settings.py:313
msgid "Spanish"
msgstr "Spanish"
#: paperless/settings.py:314
msgid "Polish"
msgstr "Polish"
#: paperless/settings.py:315
msgid "Swedish"
msgstr "Swedish"
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "Paperless-ng administration"
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr "Authentication"
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr "Advanced settings"
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Filter"
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless will only process mails that match ALL of the filters given below."
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Actions"
#: paperless_mail/admin.py:51
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 "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."
#: paperless_mail/admin.py:58
msgid "Metadata"
msgstr "Metadata"
#: paperless_mail/admin.py:60
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 "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."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Paperless mail"
#: paperless_mail/models.py:11
msgid "mail account"
msgstr "mail account"
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr "mail accounts"
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr "No encryption"
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr "Use SSL"
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr "Use STARTTLS"
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr "IMAP server"
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr "IMAP port"
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr "IMAP security"
#: paperless_mail/models.py:46
msgid "username"
msgstr "username"
#: paperless_mail/models.py:50
msgid "password"
msgstr "password"
#: paperless_mail/models.py:54
msgid "character set"
msgstr ""
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr ""
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "mail rule"
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "mail rules"
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "Only process attachments."
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "Process all files, including 'inline' attachments."
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "Mark as read, don't process read mails"
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "Flag the mail, don't process flagged mails"
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "Move to specified folder"
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Delete"
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "Use subject as title"
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "Use attachment filename as title"
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "Do not assign a correspondent"
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "Use mail address"
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "Use name (or mail address if not available)"
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "Use correspondent selected below"
#: paperless_mail/models.py:121
msgid "order"
msgstr "order"
#: paperless_mail/models.py:128
msgid "account"
msgstr "account"
#: paperless_mail/models.py:132
msgid "folder"
msgstr "folder"
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr ""
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "filter from"
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "filter subject"
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "filter body"
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "filter attachment filename"
#: paperless_mail/models.py:150
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
#: paperless_mail/models.py:156
msgid "maximum age"
msgstr "maximum age"
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "Specified in days."
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "attachment type"
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
#: paperless_mail/models.py:169
msgid "action"
msgstr "action"
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "action parameter"
#: paperless_mail/models.py:177
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:184
msgid "assign title from"
msgstr "assign title from"
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "assign this tag"
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "assign this document type"
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "assign correspondent from"
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "assign this correspondent"

View File

@@ -0,0 +1,718 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr ""
#: documents/models.py:45
msgid "match"
msgstr ""
#: documents/models.py:49
msgid "matching algorithm"
msgstr ""
#: documents/models.py:55
msgid "is insensitive"
msgstr ""
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr ""
#: documents/models.py:75
msgid "correspondents"
msgstr ""
#: documents/models.py:81
msgid "color"
msgstr ""
#: documents/models.py:87
msgid "is inbox tag"
msgstr ""
#: documents/models.py:89
msgid ""
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
"with inbox tags."
msgstr ""
#: documents/models.py:94
msgid "tag"
msgstr ""
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr ""
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr ""
#: documents/models.py:102
msgid "document types"
msgstr ""
#: documents/models.py:110
msgid "Unencrypted"
msgstr ""
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr ""
#: documents/models.py:124
msgid "title"
msgstr ""
#: documents/models.py:137
msgid "content"
msgstr ""
#: documents/models.py:139
msgid ""
"The raw, text-only data of the document. This field is primarily used for "
"searching."
msgstr ""
#: documents/models.py:144
msgid "mime type"
msgstr ""
#: documents/models.py:155
msgid "checksum"
msgstr ""
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr ""
#: documents/models.py:163
msgid "archive checksum"
msgstr ""
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr ""
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr ""
#: documents/models.py:176
msgid "modified"
msgstr ""
#: documents/models.py:180
msgid "storage type"
msgstr ""
#: documents/models.py:188
msgid "added"
msgstr ""
#: documents/models.py:192
msgid "filename"
msgstr ""
#: documents/models.py:198
msgid "Current filename in storage"
msgstr ""
#: documents/models.py:202
msgid "archive filename"
msgstr ""
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr ""
#: documents/models.py:212
msgid "archive serial number"
msgstr ""
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr ""
#: documents/models.py:223
msgid "document"
msgstr ""
#: documents/models.py:224
msgid "documents"
msgstr ""
#: documents/models.py:311
msgid "debug"
msgstr ""
#: documents/models.py:312
msgid "information"
msgstr ""
#: documents/models.py:313
msgid "warning"
msgstr ""
#: documents/models.py:314
msgid "error"
msgstr ""
#: documents/models.py:315
msgid "critical"
msgstr ""
#: documents/models.py:319
msgid "group"
msgstr ""
#: documents/models.py:322
msgid "message"
msgstr ""
#: documents/models.py:325
msgid "level"
msgstr ""
#: documents/models.py:332
msgid "log"
msgstr ""
#: documents/models.py:333
msgid "logs"
msgstr ""
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr ""
#: documents/models.py:345
msgid "saved views"
msgstr ""
#: documents/models.py:348
msgid "user"
msgstr ""
#: documents/models.py:354
msgid "show on dashboard"
msgstr ""
#: documents/models.py:357
msgid "show in sidebar"
msgstr ""
#: documents/models.py:361
msgid "sort field"
msgstr ""
#: documents/models.py:367
msgid "sort reverse"
msgstr ""
#: documents/models.py:373
msgid "title contains"
msgstr ""
#: documents/models.py:374
msgid "content contains"
msgstr ""
#: documents/models.py:375
msgid "ASN is"
msgstr ""
#: documents/models.py:376
msgid "correspondent is"
msgstr ""
#: documents/models.py:377
msgid "document type is"
msgstr ""
#: documents/models.py:378
msgid "is in inbox"
msgstr ""
#: documents/models.py:379
msgid "has tag"
msgstr ""
#: documents/models.py:380
msgid "has any tag"
msgstr ""
#: documents/models.py:381
msgid "created before"
msgstr ""
#: documents/models.py:382
msgid "created after"
msgstr ""
#: documents/models.py:383
msgid "created year is"
msgstr ""
#: documents/models.py:384
msgid "created month is"
msgstr ""
#: documents/models.py:385
msgid "created day is"
msgstr ""
#: documents/models.py:386
msgid "added before"
msgstr ""
#: documents/models.py:387
msgid "added after"
msgstr ""
#: documents/models.py:388
msgid "modified before"
msgstr ""
#: documents/models.py:389
msgid "modified after"
msgstr ""
#: documents/models.py:390
msgid "does not have tag"
msgstr ""
#: documents/models.py:391
msgid "does not have ASN"
msgstr ""
#: documents/models.py:392
msgid "title or content contains"
msgstr ""
#: documents/models.py:393
msgid "fulltext query"
msgstr ""
#: documents/models.py:394
msgid "more like this"
msgstr ""
#: documents/models.py:405
msgid "rule type"
msgstr ""
#: documents/models.py:409
msgid "value"
msgstr ""
#: documents/models.py:415
msgid "filter rule"
msgstr ""
#: documents/models.py:416
msgid "filter rules"
msgstr ""
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr ""
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr ""
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr ""
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr ""
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr ""
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr ""
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr ""
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr ""
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr ""
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr ""
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr ""
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr ""
#: paperless/settings.py:303
msgid "English (US)"
msgstr ""
#: paperless/settings.py:304
msgid "English (GB)"
msgstr ""
#: paperless/settings.py:305
msgid "German"
msgstr ""
#: paperless/settings.py:306
msgid "Dutch"
msgstr ""
#: paperless/settings.py:307
msgid "French"
msgstr ""
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr ""
#: paperless/settings.py:309
msgid "Portuguese"
msgstr ""
#: paperless/settings.py:310
msgid "Italian"
msgstr ""
#: paperless/settings.py:311
msgid "Romanian"
msgstr ""
#: paperless/settings.py:312
msgid "Russian"
msgstr ""
#: paperless/settings.py:313
msgid "Spanish"
msgstr ""
#: paperless/settings.py:314
msgid "Polish"
msgstr ""
#: paperless/settings.py:315
msgid "Swedish"
msgstr ""
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr ""
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr ""
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr ""
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr ""
#: paperless_mail/admin.py:39
msgid ""
"Paperless will only process mails that match ALL of the filters given below."
msgstr ""
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr ""
#: paperless_mail/admin.py:51
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:58
msgid "Metadata"
msgstr ""
#: paperless_mail/admin.py:60
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_mail/apps.py:9
msgid "Paperless mail"
msgstr ""
#: paperless_mail/models.py:11
msgid "mail account"
msgstr ""
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr ""
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr ""
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr ""
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr ""
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr ""
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr ""
#: paperless_mail/models.py:36
msgid ""
"This is usually 143 for unencrypted and STARTTLS connections, and 993 for "
"SSL connections."
msgstr ""
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr ""
#: paperless_mail/models.py:46
msgid "username"
msgstr ""
#: paperless_mail/models.py:50
msgid "password"
msgstr ""
#: paperless_mail/models.py:54
msgid "character set"
msgstr ""
#: paperless_mail/models.py:57
msgid ""
"The character set to use when communicating with the mail server, such as "
"'UTF-8' or 'US-ASCII'."
msgstr ""
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr ""
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr ""
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr ""
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr ""
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr ""
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr ""
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr ""
#: paperless_mail/models.py:89
msgid "Delete"
msgstr ""
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr ""
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr ""
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr ""
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr ""
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr ""
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr ""
#: paperless_mail/models.py:121
msgid "order"
msgstr ""
#: paperless_mail/models.py:128
msgid "account"
msgstr ""
#: paperless_mail/models.py:132
msgid "folder"
msgstr ""
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr ""
#: paperless_mail/models.py:138
msgid "filter from"
msgstr ""
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr ""
#: paperless_mail/models.py:144
msgid "filter body"
msgstr ""
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr ""
#: paperless_mail/models.py:150
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:156
msgid "maximum age"
msgstr ""
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr ""
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr ""
#: paperless_mail/models.py:164
msgid ""
"Inline attachments include embedded images, so it's best to combine this "
"option with a filename filter."
msgstr ""
#: paperless_mail/models.py:169
msgid "action"
msgstr ""
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr ""
#: paperless_mail/models.py:177
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:184
msgid "assign title from"
msgstr ""
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr ""
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr ""
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr ""
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr ""

View File

@@ -0,0 +1,698 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-07-29 20:57\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\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-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: es-ES\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Documentos"
#: documents/models.py:32
msgid "Any word"
msgstr "Cualquier palabra"
#: documents/models.py:33
msgid "All words"
msgstr "Todas las palabras"
#: documents/models.py:34
msgid "Exact match"
msgstr "Coincidencia exacta"
#: documents/models.py:35
msgid "Regular expression"
msgstr "Expresión regular"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Palabra borrosa"
#: documents/models.py:37
msgid "Automatic"
msgstr "Automático"
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "nombre"
#: documents/models.py:45
msgid "match"
msgstr "coincidencia"
#: documents/models.py:49
msgid "matching algorithm"
msgstr "Algoritmo de coincidencia"
#: documents/models.py:55
msgid "is insensitive"
msgstr "es insensible"
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "interlocutor"
#: documents/models.py:75
msgid "correspondents"
msgstr "interlocutores"
#: documents/models.py:81
msgid "color"
msgstr "color"
#: documents/models.py:87
msgid "is inbox tag"
msgstr "es etiqueta de bandeja"
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Marca esta etiqueta como una etiqueta de bandeja: todos los documentos recién consumidos serán etiquetados con las etiquetas de bandeja."
#: documents/models.py:94
msgid "tag"
msgstr "etiqueta"
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "etiquetas"
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "tipo de documento"
#: documents/models.py:102
msgid "document types"
msgstr "tipos de documento"
#: documents/models.py:110
msgid "Unencrypted"
msgstr "Sin cifrar"
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "Cifrado con GNU Privacy Guard"
#: documents/models.py:124
msgid "title"
msgstr "título"
#: documents/models.py:137
msgid "content"
msgstr "contenido"
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "Los datos de texto en bruto del documento. Este campo se utiliza principalmente para las búsquedas."
#: documents/models.py:144
msgid "mime type"
msgstr "tipo MIME"
#: documents/models.py:155
msgid "checksum"
msgstr "Cadena de verificación"
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "La cadena de verificación del documento original."
#: documents/models.py:163
msgid "archive checksum"
msgstr "cadena de comprobación del archivo"
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "La cadena de verificación del documento archivado."
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "creado"
#: documents/models.py:176
msgid "modified"
msgstr "modificado"
#: documents/models.py:180
msgid "storage type"
msgstr "tipo de almacenamiento"
#: documents/models.py:188
msgid "added"
msgstr "añadido"
#: documents/models.py:192
msgid "filename"
msgstr "nombre del archivo"
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "Nombre de archivo actual en disco"
#: documents/models.py:202
msgid "archive filename"
msgstr "nombre de archivo"
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr "Nombre de archivo actual en disco"
#: documents/models.py:212
msgid "archive serial number"
msgstr "número de serie del archivo"
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr "Posición de este documento en tu archivo físico de documentos."
#: documents/models.py:223
msgid "document"
msgstr "documento"
#: documents/models.py:224
msgid "documents"
msgstr "documentos"
#: documents/models.py:311
msgid "debug"
msgstr "depuración"
#: documents/models.py:312
msgid "information"
msgstr "información"
#: documents/models.py:313
msgid "warning"
msgstr "alerta"
#: documents/models.py:314
msgid "error"
msgstr "error"
#: documents/models.py:315
msgid "critical"
msgstr "crítico"
#: documents/models.py:319
msgid "group"
msgstr "grupo"
#: documents/models.py:322
msgid "message"
msgstr "mensaje"
#: documents/models.py:325
msgid "level"
msgstr "nivel"
#: documents/models.py:332
msgid "log"
msgstr "log"
#: documents/models.py:333
msgid "logs"
msgstr "logs"
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "vista guardada"
#: documents/models.py:345
msgid "saved views"
msgstr "vistas guardadas"
#: documents/models.py:348
msgid "user"
msgstr "usuario"
#: documents/models.py:354
msgid "show on dashboard"
msgstr "mostrar en el panel de control"
#: documents/models.py:357
msgid "show in sidebar"
msgstr "mostrar en barra lateral"
#: documents/models.py:361
msgid "sort field"
msgstr "campo de ordenación"
#: documents/models.py:367
msgid "sort reverse"
msgstr "ordenar al revés"
#: documents/models.py:373
msgid "title contains"
msgstr "el título contiene"
#: documents/models.py:374
msgid "content contains"
msgstr "el contenido contiene"
#: documents/models.py:375
msgid "ASN is"
msgstr "ASN es"
#: documents/models.py:376
msgid "correspondent is"
msgstr "interlocutor es"
#: documents/models.py:377
msgid "document type is"
msgstr "el tipo de documento es"
#: documents/models.py:378
msgid "is in inbox"
msgstr "está en la bandeja de entrada"
#: documents/models.py:379
msgid "has tag"
msgstr "tiene la etiqueta"
#: documents/models.py:380
msgid "has any tag"
msgstr "tiene cualquier etiqueta"
#: documents/models.py:381
msgid "created before"
msgstr "creado antes"
#: documents/models.py:382
msgid "created after"
msgstr "creado después"
#: documents/models.py:383
msgid "created year is"
msgstr "el año de creación es"
#: documents/models.py:384
msgid "created month is"
msgstr "el mes de creación es"
#: documents/models.py:385
msgid "created day is"
msgstr "creado el día"
#: documents/models.py:386
msgid "added before"
msgstr "agregado antes de"
#: documents/models.py:387
msgid "added after"
msgstr "agregado después de"
#: documents/models.py:388
msgid "modified before"
msgstr "modificado después de"
#: documents/models.py:389
msgid "modified after"
msgstr "modificado antes de"
#: documents/models.py:390
msgid "does not have tag"
msgstr "no tiene la etiqueta"
#: documents/models.py:391
msgid "does not have ASN"
msgstr "no tiene ASN"
#: documents/models.py:392
msgid "title or content contains"
msgstr "el título o cuerpo contiene"
#: documents/models.py:393
msgid "fulltext query"
msgstr "consulta de texto completo"
#: documents/models.py:394
msgid "more like this"
msgstr "más contenido similar"
#: documents/models.py:405
msgid "rule type"
msgstr "tipo de regla"
#: documents/models.py:409
msgid "value"
msgstr "valor"
#: documents/models.py:415
msgid "filter rule"
msgstr "regla de filtrado"
#: documents/models.py:416
msgid "filter rules"
msgstr "reglas de filtrado"
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "Expresión irregular inválida: %(error)s"
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr "Color inválido."
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "Tipo de fichero %(type)s no suportado"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "Paperless-ng está cargándose..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "Paperless-ng Sesión cerrada"
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "Has cerrado la sesión satisfactoriamente. ¡Adiós!"
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "Iniciar sesión de nuevo"
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "Paperless-ng Iniciar sesión"
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "Por favor, inicie sesión"
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr "Tu usuario y contraseña no coinciden. Inténtalo de nuevo."
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "Usuario"
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "Contraseña"
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "Iniciar sesión"
#: paperless/settings.py:303
msgid "English (US)"
msgstr "Inglés (US)"
#: paperless/settings.py:304
msgid "English (GB)"
msgstr "Inglés (Gran Bretaña)"
#: paperless/settings.py:305
msgid "German"
msgstr "Alemán"
#: paperless/settings.py:306
msgid "Dutch"
msgstr "Alemán"
#: paperless/settings.py:307
msgid "French"
msgstr "Francés"
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr "Portugués (Brasil)"
#: paperless/settings.py:309
msgid "Portuguese"
msgstr "Portugués"
#: paperless/settings.py:310
msgid "Italian"
msgstr "Italiano"
#: paperless/settings.py:311
msgid "Romanian"
msgstr "Rumano"
#: paperless/settings.py:312
msgid "Russian"
msgstr "Ruso"
#: paperless/settings.py:313
msgid "Spanish"
msgstr "Español"
#: paperless/settings.py:314
msgid "Polish"
msgstr "Polaco"
#: paperless/settings.py:315
msgid "Swedish"
msgstr "Sueco"
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "Paperless-ng Administración"
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr "Autentificación"
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr "Configuración avanzada"
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Filtro"
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless solo procesará los correos que coincidan con TODOS los filtros escritos abajo."
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Acciones"
#: paperless_mail/admin.py:51
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 "La acción se aplicó al correo. Esta acción sólo se realiza cuando los documentos se consumen desde el correo. Los correos sin archivos adjuntos permanecerán totalmente intactos."
#: paperless_mail/admin.py:58
msgid "Metadata"
msgstr "Metadatos"
#: paperless_mail/admin.py:60
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 "Asignar metadatos a documentos consumidos por esta regla automáticamente. Si no asigna etiquetas, tipos o interlocutores aquí, paperless procesará igualmente todas las reglas que haya definido."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Correo Paperless"
#: paperless_mail/models.py:11
msgid "mail account"
msgstr "cuenta de correo"
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr "cuentas de correo"
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr "Sin encriptar"
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr "Usar SSL"
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr "Usar STARTTLS"
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr "Servidor IMAP"
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr "Puerto IMAP"
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "Normalmente 143 para conexiones sin encriptar y STARTTLS, y 993 para conexiones SSL."
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr "Seguridad IMAP"
#: paperless_mail/models.py:46
msgid "username"
msgstr "usuario"
#: paperless_mail/models.py:50
msgid "password"
msgstr "contraseña"
#: paperless_mail/models.py:54
msgid "character set"
msgstr "conjunto de caracteres"
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr "El conjunto de caracteres a usar al comunicarse con el servidor de correo, como 'UTF-8' o 'US-ASCII'."
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "regla de correo"
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "reglas de correo"
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "Solo procesar ficheros adjuntos."
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "Procesar todos los ficheros, incluyendo ficheros 'incrustados'."
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "Marcar como leído, no procesar archivos leídos"
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "Marcar el correo, no procesar correos marcados"
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "Mover a carpeta específica"
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Borrar"
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "Usar asunto como título"
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "Usar nombre del fichero adjunto como título"
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "No asignar interlocutor"
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "Usar dirección de correo"
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "Usar nombre (o dirección de correo sino está disponible)"
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "Usar el interlocutor seleccionado a continuación"
#: paperless_mail/models.py:121
msgid "order"
msgstr "orden"
#: paperless_mail/models.py:128
msgid "account"
msgstr "cuenta"
#: paperless_mail/models.py:132
msgid "folder"
msgstr "carpeta"
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr "Las subcarpetas deben estar separadas por puntos."
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "filtrar desde"
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "filtrar asunto"
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "filtrar cuerpo"
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "filtrar nombre del fichero adjunto"
#: paperless_mail/models.py:150
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "Sólo consumirá documentos que coincidan completamente con este nombre de archivo si se especifica. Se permiten comodines como *.pdf o *factura*. No diferencia mayúsculas."
#: paperless_mail/models.py:156
msgid "maximum age"
msgstr "antigüedad máxima"
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "Especificado en días."
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "tipo de fichero adjunto"
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "Adjuntos incrustados incluyen imágenes, por lo que es mejor combina resta opción un filtro de nombre de fichero."
#: paperless_mail/models.py:169
msgid "action"
msgstr "acción"
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "parámetro de acción"
#: paperless_mail/models.py:177
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 "Parámetro adicional para la acción seleccionada arriba. Ej. la carpeta de destino de la acción \"mover a carpeta\". Las subcarpetas deben estar separadas por puntos."
#: paperless_mail/models.py:184
msgid "assign title from"
msgstr "asignar título desde"
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "asignar esta etiqueta"
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "asignar este tipo de documento"
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "asignar interlocutor desde"
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "asignar este interlocutor"

View File

@@ -1,26 +1,21 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jonas Winkler, 2020
# Philmo67, 2021
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-10 21:41+0000\n"
"PO-Revision-Date: 2020-12-30 19:27+0000\n"
"Last-Translator: Philmo67, 2021\n"
"Language-Team: French (https://www.transifex.com/paperless/teams/115905/fr/)\n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-05-17 13:13\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fr\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Crowdin-Project: paperless-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: fr\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
@@ -50,8 +45,8 @@ msgstr "Mot approximatif"
msgid "Automatic"
msgstr "Automatique"
#: documents/models.py:41 documents/models.py:354 paperless_mail/models.py:25
#: paperless_mail/models.py:109
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "nom"
@@ -67,388 +62,443 @@ msgstr "algorithme de rapprochement"
msgid "is insensitive"
msgstr "est insensible à la casse"
#: documents/models.py:80 documents/models.py:140
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "correspondant"
#: documents/models.py:81
#: documents/models.py:75
msgid "correspondents"
msgstr "correspondants"
#: documents/models.py:103
#: documents/models.py:81
msgid "color"
msgstr "couleur"
#: documents/models.py:107
#: documents/models.py:87
msgid "is inbox tag"
msgstr "est une étiquette de boîte de réception"
#: documents/models.py:109
msgid ""
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
"with inbox tags."
msgstr ""
"Marque cette étiquette comme étiquette de boîte de réception : ces "
"étiquettes sont affectées à tous les documents nouvellement traités."
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Marque cette étiquette comme étiquette de boîte de réception : ces étiquettes sont affectées à tous les documents nouvellement traités."
#: documents/models.py:114
#: documents/models.py:94
msgid "tag"
msgstr "étiquette"
#: documents/models.py:115 documents/models.py:171
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "étiquettes"
#: documents/models.py:121 documents/models.py:153
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "type de document"
#: documents/models.py:122
#: documents/models.py:102
msgid "document types"
msgstr "types de document"
#: documents/models.py:130
#: documents/models.py:110
msgid "Unencrypted"
msgstr "Non chiffré"
#: documents/models.py:131
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "Chiffré avec GNU Privacy Guard"
#: documents/models.py:144
#: documents/models.py:124
msgid "title"
msgstr "titre"
#: documents/models.py:157
#: documents/models.py:137
msgid "content"
msgstr "contenu"
#: documents/models.py:159
msgid ""
"The raw, text-only data of the document. This field is primarily used for "
"searching."
msgstr ""
"Les données brutes du document, en format texte uniquement. Ce champ est "
"principalement utilisé pour la recherche."
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "Les données brutes du document, en format texte uniquement. Ce champ est principalement utilisé pour la recherche."
#: documents/models.py:164
#: documents/models.py:144
msgid "mime type"
msgstr "type mime"
#: documents/models.py:175
#: documents/models.py:155
msgid "checksum"
msgstr "somme de contrôle"
#: documents/models.py:179
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "La somme de contrôle du document original."
#: documents/models.py:183
#: documents/models.py:163
msgid "archive checksum"
msgstr "somme de contrôle de l'archive"
#: documents/models.py:188
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "La somme de contrôle du document archivé."
#: documents/models.py:192 documents/models.py:332
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "créé le"
#: documents/models.py:196
#: documents/models.py:176
msgid "modified"
msgstr "modifié"
#: documents/models.py:200
#: documents/models.py:180
msgid "storage type"
msgstr "forme d'enregistrement :"
#: documents/models.py:208
#: documents/models.py:188
msgid "added"
msgstr "date d'ajout"
#: documents/models.py:212
#: documents/models.py:192
msgid "filename"
msgstr "nom du fichier"
#: documents/models.py:217
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "Nom du fichier courant en base de données"
#: documents/models.py:221
#: documents/models.py:202
msgid "archive filename"
msgstr "nom de fichier de l'archive"
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr "Nom du fichier d'archive courant en base de données"
#: documents/models.py:212
msgid "archive serial number"
msgstr "numéro de série de l'archive"
#: documents/models.py:226
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr ""
"Le classement de ce document dans votre archive de documents physiques."
msgstr "Le classement de ce document dans votre archive de documents physiques."
#: documents/models.py:232
#: documents/models.py:223
msgid "document"
msgstr "document"
#: documents/models.py:233
#: documents/models.py:224
msgid "documents"
msgstr "documents"
#: documents/models.py:315
#: documents/models.py:311
msgid "debug"
msgstr "débogage"
#: documents/models.py:316
#: documents/models.py:312
msgid "information"
msgstr "information"
#: documents/models.py:317
#: documents/models.py:313
msgid "warning"
msgstr "avertissement"
#: documents/models.py:318
#: documents/models.py:314
msgid "error"
msgstr "erreur"
#: documents/models.py:319
#: documents/models.py:315
msgid "critical"
msgstr "critique"
#: documents/models.py:323
#: documents/models.py:319
msgid "group"
msgstr "groupe"
#: documents/models.py:326
#: documents/models.py:322
msgid "message"
msgstr "message"
#: documents/models.py:329
#: documents/models.py:325
msgid "level"
msgstr "niveau"
#: documents/models.py:336
#: documents/models.py:332
msgid "log"
msgstr "rapport"
msgstr "journal"
#: documents/models.py:337
#: documents/models.py:333
msgid "logs"
msgstr "rapports"
msgstr "journaux"
#: documents/models.py:348 documents/models.py:398
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "vue enregistrée"
#: documents/models.py:349
#: documents/models.py:345
msgid "saved views"
msgstr "vues enregistrées"
#: documents/models.py:352
#: documents/models.py:348
msgid "user"
msgstr "utilisateur"
#: documents/models.py:358
#: documents/models.py:354
msgid "show on dashboard"
msgstr "montrer sur le tableau de bord"
#: documents/models.py:361
#: documents/models.py:357
msgid "show in sidebar"
msgstr "montrer dans la barre latérale"
#: documents/models.py:365
#: documents/models.py:361
msgid "sort field"
msgstr "champ de tri"
#: documents/models.py:368
#: documents/models.py:367
msgid "sort reverse"
msgstr "tri inverse"
#: documents/models.py:374
#: documents/models.py:373
msgid "title contains"
msgstr "le titre contient"
#: documents/models.py:375
#: documents/models.py:374
msgid "content contains"
msgstr "le contenu contient"
#: documents/models.py:376
#: documents/models.py:375
msgid "ASN is"
msgstr "le NSA est"
#: documents/models.py:377
#: documents/models.py:376
msgid "correspondent is"
msgstr "le correspondant est"
#: documents/models.py:378
#: documents/models.py:377
msgid "document type is"
msgstr "le type de document est"
#: documents/models.py:379
#: documents/models.py:378
msgid "is in inbox"
msgstr "est dans la boîte de réception"
#: documents/models.py:380
#: documents/models.py:379
msgid "has tag"
msgstr "porte l'étiquette"
#: documents/models.py:381
#: documents/models.py:380
msgid "has any tag"
msgstr "porte l'une des étiquettes"
#: documents/models.py:382
#: documents/models.py:381
msgid "created before"
msgstr "créé avant"
#: documents/models.py:383
#: documents/models.py:382
msgid "created after"
msgstr "créé après"
#: documents/models.py:384
#: documents/models.py:383
msgid "created year is"
msgstr "l'année de création est"
#: documents/models.py:385
#: documents/models.py:384
msgid "created month is"
msgstr "le mois de création est"
#: documents/models.py:386
#: documents/models.py:385
msgid "created day is"
msgstr "le jour de création est"
#: documents/models.py:387
#: documents/models.py:386
msgid "added before"
msgstr "ajouté avant"
#: documents/models.py:388
#: documents/models.py:387
msgid "added after"
msgstr "ajouté après"
#: documents/models.py:389
#: documents/models.py:388
msgid "modified before"
msgstr "modifié avant"
#: documents/models.py:390
#: documents/models.py:389
msgid "modified after"
msgstr "modifié après"
#: documents/models.py:391
#: documents/models.py:390
msgid "does not have tag"
msgstr "ne porte pas d'étiquette"
#: documents/models.py:402
#: documents/models.py:391
msgid "does not have ASN"
msgstr "ne porte pas de NSA"
#: documents/models.py:392
msgid "title or content contains"
msgstr "le titre ou le contenu contient"
#: documents/models.py:393
msgid "fulltext query"
msgstr "recherche en texte intégral"
#: documents/models.py:394
msgid "more like this"
msgstr "documents relatifs"
#: documents/models.py:405
msgid "rule type"
msgstr "type de règle"
#: documents/models.py:406
#: documents/models.py:409
msgid "value"
msgstr "valeur"
#: documents/models.py:412
#: documents/models.py:415
msgid "filter rule"
msgstr "règle de filtrage"
#: documents/models.py:413
#: documents/models.py:416
msgid "filter rules"
msgstr "règles de filtrage"
#: documents/templates/index.html:20
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "Expression régulière incorrecte : %(error)s"
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr "Couleur incorrecte."
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "Type de fichier %(type)s non pris en charge"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "Paperless-ng est en cours de chargement..."
#: documents/templates/registration/logged_out.html:13
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "Déconnecté de Paperless-ng"
#: documents/templates/registration/logged_out.html:41
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "Vous avez été déconnecté avec succès. Au revoir !"
#: documents/templates/registration/logged_out.html:42
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "Se reconnecter"
#: documents/templates/registration/login.html:13
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "Connexion à Paperless-ng"
#: documents/templates/registration/login.html:42
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "Veuillez vous connecter."
#: documents/templates/registration/login.html:45
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr ""
"Votre nom d'utilisateur et votre mot de passe ne correspondent pas. Veuillez"
" réessayer."
msgstr "Votre nom d'utilisateur et votre mot de passe ne correspondent pas. Veuillez réessayer."
#: documents/templates/registration/login.html:48
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "Nom d'utilisateur"
#: documents/templates/registration/login.html:49
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "Mot de passe"
#: documents/templates/registration/login.html:54
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "S'identifier"
#: paperless/settings.py:268
msgid "English"
msgstr "Anglais"
#: paperless/settings.py:303
msgid "English (US)"
msgstr "Anglais (US)"
#: paperless/settings.py:269
#: paperless/settings.py:304
msgid "English (GB)"
msgstr "Anglais (GB)"
#: paperless/settings.py:305
msgid "German"
msgstr "Allemand"
#: paperless/settings.py:270
#: paperless/settings.py:306
msgid "Dutch"
msgstr "Néerlandais"
#: paperless/settings.py:271
#: paperless/settings.py:307
msgid "French"
msgstr "Français"
#: paperless/urls.py:108
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr "Portugais (Brésil)"
#: paperless/settings.py:309
msgid "Portuguese"
msgstr "Portugais"
#: paperless/settings.py:310
msgid "Italian"
msgstr "Italien"
#: paperless/settings.py:311
msgid "Romanian"
msgstr "Roumain"
#: paperless/settings.py:312
msgid "Russian"
msgstr "Russe"
#: paperless/settings.py:313
msgid "Spanish"
msgstr "Espagnol"
#: paperless/settings.py:314
msgid "Polish"
msgstr "Polonais"
#: paperless/settings.py:315
msgid "Swedish"
msgstr "Suédois"
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "Administration de Paperless-ng"
#: paperless_mail/admin.py:25
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr "Authentification"
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr "Paramètres avancés"
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Filtrage"
#: paperless_mail/admin.py:27
msgid ""
"Paperless will only process mails that match ALL of the filters given below."
msgstr ""
"Paperless-ng ne traitera que les courriers qui correspondent à TOUS les "
"filtres ci-dessous."
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless-ng ne traitera que les courriers qui correspondent à TOUS les filtres ci-dessous."
#: paperless_mail/admin.py:37
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Actions"
#: paperless_mail/admin.py:39
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 ""
"Action appliquée au courriel. Cette action n'est exécutée que lorsque les "
"documents ont été traités depuis des courriels. Les courriels sans pièces "
"jointes demeurent totalement inchangés."
#: paperless_mail/admin.py:51
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 "Action appliquée au courriel. Cette action n'est exécutée que lorsque les documents ont été traités depuis des courriels. Les courriels sans pièces jointes demeurent totalement inchangés."
#: paperless_mail/admin.py:46
#: paperless_mail/admin.py:58
msgid "Metadata"
msgstr "Métadonnées"
#: paperless_mail/admin.py:48
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 ""
"Affecter automatiquement des métadonnées aux documents traités à partir de "
"cette règle. Si vous n'affectez pas d'étiquette, de type ou de correspondant"
" ici, Paperless-ng appliquera toutes les autres règles de rapprochement que "
"vous avez définies."
#: paperless_mail/admin.py:60
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 "Affecter automatiquement des métadonnées aux documents traités à partir de cette règle. Si vous n'affectez pas d'étiquette, de type ou de correspondant ici, Paperless-ng appliquera toutes les autres règles de rapprochement que vous avez définies."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
@@ -483,12 +533,8 @@ msgid "IMAP port"
msgstr "Port IMAP"
#: paperless_mail/models.py:36
msgid ""
"This is usually 143 for unencrypted and STARTTLS connections, and 993 for "
"SSL connections."
msgstr ""
"Généralement 143 pour les connexions non chiffrées et STARTTLS, et 993 pour "
"les connexions SSL."
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "Généralement 143 pour les connexions non chiffrées et STARTTLS, et 993 pour les connexions SSL."
#: paperless_mail/models.py:40
msgid "IMAP security"
@@ -502,151 +548,151 @@ msgstr "nom d'utilisateur"
msgid "password"
msgstr "mot de passe"
#: paperless_mail/models.py:60
#: paperless_mail/models.py:54
msgid "character set"
msgstr "jeu de caractères"
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr "Le jeu de caractères à utiliser lors de la communication avec le serveur de messagerie, par exemple 'UTF-8' ou 'US-ASCII'."
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "règle de courriel"
#: paperless_mail/models.py:61
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "règles de courriel"
#: paperless_mail/models.py:67
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "Ne traiter que les pièces jointes."
#: paperless_mail/models.py:68
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "Traiter tous les fichiers, y compris les pièces jointes \"en ligne\"."
#: paperless_mail/models.py:78
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "Marquer comme lu, ne pas traiter les courriels lus"
#: paperless_mail/models.py:79
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "Marquer le courriel, ne pas traiter les courriels marqués"
#: paperless_mail/models.py:80
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "Déplacer vers le dossier spécifié"
#: paperless_mail/models.py:81
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Supprimer"
#: paperless_mail/models.py:88
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "Utiliser le sujet en tant que titre"
#: paperless_mail/models.py:89
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "Utiliser le nom de la pièce jointe en tant que titre"
#: paperless_mail/models.py:99
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "Ne pas affecter de correspondant"
#: paperless_mail/models.py:101
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "Utiliser l'adresse électronique"
#: paperless_mail/models.py:103
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "Utiliser le nom (ou l'adresse électronique s'il n'est pas disponible)"
#: paperless_mail/models.py:105
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "Utiliser le correspondant sélectionné ci-dessous"
#: paperless_mail/models.py:113
#: paperless_mail/models.py:121
msgid "order"
msgstr "ordre"
#: paperless_mail/models.py:120
#: paperless_mail/models.py:128
msgid "account"
msgstr "compte"
#: paperless_mail/models.py:124
#: paperless_mail/models.py:132
msgid "folder"
msgstr "répertoire"
#: paperless_mail/models.py:128
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr "Les sous-dossiers doivent être séparés par des points."
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "filtrer l'expéditeur"
#: paperless_mail/models.py:131
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "filtrer le sujet"
#: paperless_mail/models.py:134
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "filtrer le corps du message"
#: paperless_mail/models.py:138
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "filtrer le nom de fichier de la pièce jointe"
#: paperless_mail/models.py:140
msgid ""
"Only consume documents which entirely match this filename if specified. "
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr ""
"Ne traiter que les documents correspondant intégralement à ce nom de fichier"
" s'il est spécifié. Les jokers tels que *.pdf ou *facture* sont autorisés. "
"La casse n'est pas prise en compte."
#: paperless_mail/models.py:150
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "Ne traiter que les documents correspondant intégralement à ce nom de fichier s'il est spécifié. Les jokers tels que *.pdf ou *facture* sont autorisés. La casse n'est pas prise en compte."
#: paperless_mail/models.py:146
#: paperless_mail/models.py:156
msgid "maximum age"
msgstr "âge maximum"
#: paperless_mail/models.py:148
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "En jours."
#: paperless_mail/models.py:151
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "type de pièce jointe"
#: paperless_mail/models.py:154
msgid ""
"Inline attachments include embedded images, so it's best to combine this "
"option with a filename filter."
msgstr ""
"Les pièces jointes en ligne comprennent les images intégrées, il est donc "
"préférable de combiner cette option avec un filtre de nom de fichier."
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "Les pièces jointes en ligne comprennent les images intégrées, il est donc préférable de combiner cette option avec un filtre de nom de fichier."
#: paperless_mail/models.py:159
#: paperless_mail/models.py:169
msgid "action"
msgstr "action"
#: paperless_mail/models.py:165
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "paramètre d'action"
#: paperless_mail/models.py:167
msgid ""
"Additional parameter for the action selected above, i.e., the target folder "
"of the move to folder action."
msgstr ""
"Paramètre supplémentaire pour l'action sélectionnée ci-dessus, par exemple "
"le dossier cible de l'action de déplacement vers un dossier."
#: paperless_mail/models.py:177
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 "Paramètre supplémentaire pour l'action sélectionnée ci-dessus, par exemple le dossier cible de l'action de déplacement vers un dossier. Les sous-dossiers doivent être séparés par des points."
#: paperless_mail/models.py:173
#: paperless_mail/models.py:184
msgid "assign title from"
msgstr "affecter le titre depuis"
#: paperless_mail/models.py:183
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "affecter cette étiquette"
#: paperless_mail/models.py:191
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "affecter ce type de document"
#: paperless_mail/models.py:195
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "affecter le correspondant depuis"
#: paperless_mail/models.py:205
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "affecter ce correspondant"

View File

@@ -0,0 +1,698 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-05-16 10:09\n"
"Last-Translator: \n"
"Language-Team: Hungarian\n"
"Language: hu_HU\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-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: hu\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Dokumentumok"
#: 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 "Regexp"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr ""
#: documents/models.py:37
msgid "Automatic"
msgstr "Automatikus"
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr ""
#: documents/models.py:45
msgid "match"
msgstr ""
#: documents/models.py:49
msgid "matching algorithm"
msgstr ""
#: documents/models.py:55
msgid "is insensitive"
msgstr ""
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr ""
#: documents/models.py:75
msgid "correspondents"
msgstr ""
#: documents/models.py:81
msgid "color"
msgstr ""
#: documents/models.py:87
msgid "is inbox tag"
msgstr ""
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr ""
#: documents/models.py:94
msgid "tag"
msgstr ""
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr ""
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr ""
#: documents/models.py:102
msgid "document types"
msgstr ""
#: documents/models.py:110
msgid "Unencrypted"
msgstr ""
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr ""
#: documents/models.py:124
msgid "title"
msgstr ""
#: documents/models.py:137
msgid "content"
msgstr ""
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr ""
#: documents/models.py:144
msgid "mime type"
msgstr ""
#: documents/models.py:155
msgid "checksum"
msgstr ""
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr ""
#: documents/models.py:163
msgid "archive checksum"
msgstr ""
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr ""
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr ""
#: documents/models.py:176
msgid "modified"
msgstr ""
#: documents/models.py:180
msgid "storage type"
msgstr ""
#: documents/models.py:188
msgid "added"
msgstr ""
#: documents/models.py:192
msgid "filename"
msgstr ""
#: documents/models.py:198
msgid "Current filename in storage"
msgstr ""
#: documents/models.py:202
msgid "archive filename"
msgstr ""
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr ""
#: documents/models.py:212
msgid "archive serial number"
msgstr ""
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr ""
#: documents/models.py:223
msgid "document"
msgstr ""
#: documents/models.py:224
msgid "documents"
msgstr ""
#: documents/models.py:311
msgid "debug"
msgstr ""
#: documents/models.py:312
msgid "information"
msgstr ""
#: documents/models.py:313
msgid "warning"
msgstr ""
#: documents/models.py:314
msgid "error"
msgstr ""
#: documents/models.py:315
msgid "critical"
msgstr ""
#: documents/models.py:319
msgid "group"
msgstr ""
#: documents/models.py:322
msgid "message"
msgstr ""
#: documents/models.py:325
msgid "level"
msgstr ""
#: documents/models.py:332
msgid "log"
msgstr ""
#: documents/models.py:333
msgid "logs"
msgstr ""
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr ""
#: documents/models.py:345
msgid "saved views"
msgstr ""
#: documents/models.py:348
msgid "user"
msgstr ""
#: documents/models.py:354
msgid "show on dashboard"
msgstr ""
#: documents/models.py:357
msgid "show in sidebar"
msgstr ""
#: documents/models.py:361
msgid "sort field"
msgstr ""
#: documents/models.py:367
msgid "sort reverse"
msgstr ""
#: documents/models.py:373
msgid "title contains"
msgstr ""
#: documents/models.py:374
msgid "content contains"
msgstr ""
#: documents/models.py:375
msgid "ASN is"
msgstr ""
#: documents/models.py:376
msgid "correspondent is"
msgstr ""
#: documents/models.py:377
msgid "document type is"
msgstr ""
#: documents/models.py:378
msgid "is in inbox"
msgstr ""
#: documents/models.py:379
msgid "has tag"
msgstr ""
#: documents/models.py:380
msgid "has any tag"
msgstr ""
#: documents/models.py:381
msgid "created before"
msgstr ""
#: documents/models.py:382
msgid "created after"
msgstr ""
#: documents/models.py:383
msgid "created year is"
msgstr ""
#: documents/models.py:384
msgid "created month is"
msgstr ""
#: documents/models.py:385
msgid "created day is"
msgstr ""
#: documents/models.py:386
msgid "added before"
msgstr ""
#: documents/models.py:387
msgid "added after"
msgstr ""
#: documents/models.py:388
msgid "modified before"
msgstr ""
#: documents/models.py:389
msgid "modified after"
msgstr ""
#: documents/models.py:390
msgid "does not have tag"
msgstr ""
#: documents/models.py:391
msgid "does not have ASN"
msgstr ""
#: documents/models.py:392
msgid "title or content contains"
msgstr ""
#: documents/models.py:393
msgid "fulltext query"
msgstr ""
#: documents/models.py:394
msgid "more like this"
msgstr ""
#: documents/models.py:405
msgid "rule type"
msgstr ""
#: documents/models.py:409
msgid "value"
msgstr ""
#: documents/models.py:415
msgid "filter rule"
msgstr ""
#: documents/models.py:416
msgid "filter rules"
msgstr ""
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr ""
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr ""
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr ""
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr ""
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr ""
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr ""
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr ""
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr ""
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr ""
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr ""
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr ""
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr ""
#: paperless/settings.py:303
msgid "English (US)"
msgstr "Angol (US)"
#: paperless/settings.py:304
msgid "English (GB)"
msgstr ""
#: paperless/settings.py:305
msgid "German"
msgstr "Német"
#: paperless/settings.py:306
msgid "Dutch"
msgstr ""
#: paperless/settings.py:307
msgid "French"
msgstr ""
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr ""
#: paperless/settings.py:309
msgid "Portuguese"
msgstr ""
#: paperless/settings.py:310
msgid "Italian"
msgstr ""
#: paperless/settings.py:311
msgid "Romanian"
msgstr ""
#: paperless/settings.py:312
msgid "Russian"
msgstr ""
#: paperless/settings.py:313
msgid "Spanish"
msgstr ""
#: paperless/settings.py:314
msgid "Polish"
msgstr ""
#: paperless/settings.py:315
msgid "Swedish"
msgstr ""
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr ""
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr ""
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr ""
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Szűrő"
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr ""
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Műveletek"
#: paperless_mail/admin.py:51
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:58
msgid "Metadata"
msgstr "Metaadat"
#: paperless_mail/admin.py:60
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_mail/apps.py:9
msgid "Paperless mail"
msgstr ""
#: paperless_mail/models.py:11
msgid "mail account"
msgstr ""
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr ""
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr ""
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr ""
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr ""
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr ""
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr ""
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr ""
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr ""
#: paperless_mail/models.py:46
msgid "username"
msgstr ""
#: paperless_mail/models.py:50
msgid "password"
msgstr ""
#: paperless_mail/models.py:54
msgid "character set"
msgstr ""
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr ""
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr ""
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr ""
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr ""
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr ""
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr ""
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr ""
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr ""
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Törlés"
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr ""
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr ""
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr ""
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr ""
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr ""
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr ""
#: paperless_mail/models.py:121
msgid "order"
msgstr ""
#: paperless_mail/models.py:128
msgid "account"
msgstr ""
#: paperless_mail/models.py:132
msgid "folder"
msgstr ""
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr ""
#: paperless_mail/models.py:138
msgid "filter from"
msgstr ""
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr ""
#: paperless_mail/models.py:144
msgid "filter body"
msgstr ""
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr ""
#: paperless_mail/models.py:150
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:156
msgid "maximum age"
msgstr ""
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr ""
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr ""
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr ""
#: paperless_mail/models.py:169
msgid "action"
msgstr ""
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr ""
#: paperless_mail/models.py:177
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:184
msgid "assign title from"
msgstr ""
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr ""
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr ""
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr ""
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr ""

View File

@@ -0,0 +1,698 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-05-17 11:06\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\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-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: it\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Documenti"
#: documents/models.py:32
msgid "Any word"
msgstr "Qualsiasi parola"
#: documents/models.py:33
msgid "All words"
msgstr "Tutte le parole"
#: documents/models.py:34
msgid "Exact match"
msgstr "Corrispondenza esatta"
#: documents/models.py:35
msgid "Regular expression"
msgstr "Espressione regolare"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Parole fuzzy"
#: documents/models.py:37
msgid "Automatic"
msgstr "Automatico"
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "nome"
#: documents/models.py:45
msgid "match"
msgstr "corrispondenza"
#: documents/models.py:49
msgid "matching algorithm"
msgstr "algoritmo di corrispondenza"
#: documents/models.py:55
msgid "is insensitive"
msgstr "non distingue maiuscole e minuscole"
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "corrispondente"
#: documents/models.py:75
msgid "correspondents"
msgstr "corrispondenti"
#: documents/models.py:81
msgid "color"
msgstr "colore"
#: documents/models.py:87
msgid "is inbox tag"
msgstr "è tag di arrivo"
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Contrassegna questo tag come tag in arrivo: tutti i documenti elaborati verranno taggati con questo tag."
#: documents/models.py:94
msgid "tag"
msgstr "tag"
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "tag"
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "tipo di documento"
#: documents/models.py:102
msgid "document types"
msgstr "tipi di documento"
#: documents/models.py:110
msgid "Unencrypted"
msgstr "Non criptato"
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "Criptato con GNU Privacy Guard"
#: documents/models.py:124
msgid "title"
msgstr "titolo"
#: documents/models.py:137
msgid "content"
msgstr "contenuto"
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "I dati grezzi o solo testo del documento. Questo campo è usato principalmente per la ricerca."
#: documents/models.py:144
msgid "mime type"
msgstr "tipo mime"
#: documents/models.py:155
msgid "checksum"
msgstr "checksum"
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "Il checksum del documento originale."
#: documents/models.py:163
msgid "archive checksum"
msgstr "checksum dell'archivio"
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "Il checksum del documento archiviato."
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "creato il"
#: documents/models.py:176
msgid "modified"
msgstr "modificato il"
#: documents/models.py:180
msgid "storage type"
msgstr "tipo di storage"
#: documents/models.py:188
msgid "added"
msgstr "aggiunto il"
#: documents/models.py:192
msgid "filename"
msgstr "nome del file"
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "Nome del file corrente nello storage"
#: documents/models.py:202
msgid "archive filename"
msgstr "Nome file in archivio"
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr "Il nome del file nell'archiviazione"
#: documents/models.py:212
msgid "archive serial number"
msgstr "numero seriale dell'archivio"
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr "Posizione di questo documento all'interno dell'archivio fisico."
#: documents/models.py:223
msgid "document"
msgstr "documento"
#: documents/models.py:224
msgid "documents"
msgstr "documenti"
#: documents/models.py:311
msgid "debug"
msgstr "debug"
#: documents/models.py:312
msgid "information"
msgstr "informazione"
#: documents/models.py:313
msgid "warning"
msgstr "avvertimento"
#: documents/models.py:314
msgid "error"
msgstr "errore"
#: documents/models.py:315
msgid "critical"
msgstr "critico"
#: documents/models.py:319
msgid "group"
msgstr "gruppo"
#: documents/models.py:322
msgid "message"
msgstr "messaggio"
#: documents/models.py:325
msgid "level"
msgstr "livello"
#: documents/models.py:332
msgid "log"
msgstr "log"
#: documents/models.py:333
msgid "logs"
msgstr "log"
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "vista salvata"
#: documents/models.py:345
msgid "saved views"
msgstr "viste salvate"
#: documents/models.py:348
msgid "user"
msgstr "utente"
#: documents/models.py:354
msgid "show on dashboard"
msgstr "mostra sul cruscotto"
#: documents/models.py:357
msgid "show in sidebar"
msgstr "mostra nella barra laterale"
#: documents/models.py:361
msgid "sort field"
msgstr "campo di ordinamento"
#: documents/models.py:367
msgid "sort reverse"
msgstr "ordine invertito"
#: documents/models.py:373
msgid "title contains"
msgstr "il titolo contiene"
#: documents/models.py:374
msgid "content contains"
msgstr "il contenuto contiene"
#: documents/models.py:375
msgid "ASN is"
msgstr "ASN è"
#: documents/models.py:376
msgid "correspondent is"
msgstr "la corrispondenza è"
#: documents/models.py:377
msgid "document type is"
msgstr "il tipo di documento è"
#: documents/models.py:378
msgid "is in inbox"
msgstr "è in arrivo"
#: documents/models.py:379
msgid "has tag"
msgstr "ha etichetta"
#: documents/models.py:380
msgid "has any tag"
msgstr "ha qualsiasi etichetta"
#: documents/models.py:381
msgid "created before"
msgstr "creato prima del"
#: documents/models.py:382
msgid "created after"
msgstr "creato dopo il"
#: documents/models.py:383
msgid "created year is"
msgstr "l'anno di creazione è"
#: documents/models.py:384
msgid "created month is"
msgstr "il mese di creazione è"
#: documents/models.py:385
msgid "created day is"
msgstr "il giorno di creazione è"
#: documents/models.py:386
msgid "added before"
msgstr "aggiunto prima del"
#: documents/models.py:387
msgid "added after"
msgstr "aggiunto dopo il"
#: documents/models.py:388
msgid "modified before"
msgstr "modificato prima del"
#: documents/models.py:389
msgid "modified after"
msgstr "modificato dopo"
#: documents/models.py:390
msgid "does not have tag"
msgstr "non ha tag"
#: documents/models.py:391
msgid "does not have ASN"
msgstr "non ha ASN"
#: documents/models.py:392
msgid "title or content contains"
msgstr "il titolo o il contenuto contiene"
#: documents/models.py:393
msgid "fulltext query"
msgstr "query fulltext"
#: documents/models.py:394
msgid "more like this"
msgstr "altro come questo"
#: documents/models.py:405
msgid "rule type"
msgstr "tipo di regola"
#: documents/models.py:409
msgid "value"
msgstr "valore"
#: documents/models.py:415
msgid "filter rule"
msgstr "regola filtro"
#: documents/models.py:416
msgid "filter rules"
msgstr "regole filtro"
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "Espressione regolare non valida: %(error)s"
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr "Colore non valido."
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "Il tipo di file %(type)s non è supportato"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "Paperless-ng è in caricamento..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "Paperless-ng è uscito"
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "Ti sei disconnesso. A presto!"
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "Accedi nuovamente"
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "Accedi a Paperless-ng"
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "Accedi"
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr "Il nome utente e la password non sono corretti. Riprovare."
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "Nome utente"
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "Password"
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "Accedi"
#: paperless/settings.py:303
msgid "English (US)"
msgstr "Inglese (US)"
#: paperless/settings.py:304
msgid "English (GB)"
msgstr "Inglese (GB)"
#: paperless/settings.py:305
msgid "German"
msgstr "Tedesco"
#: paperless/settings.py:306
msgid "Dutch"
msgstr "Olandese"
#: paperless/settings.py:307
msgid "French"
msgstr "Francese"
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr "Portoghese (Brasile)"
#: paperless/settings.py:309
msgid "Portuguese"
msgstr "Portoghese"
#: paperless/settings.py:310
msgid "Italian"
msgstr "Italiano"
#: paperless/settings.py:311
msgid "Romanian"
msgstr "Rumeno"
#: paperless/settings.py:312
msgid "Russian"
msgstr "Russo"
#: paperless/settings.py:313
msgid "Spanish"
msgstr "Spagnolo"
#: paperless/settings.py:314
msgid "Polish"
msgstr "Polacco"
#: paperless/settings.py:315
msgid "Swedish"
msgstr "Svedese"
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "Amministrazione di Paperless-ng"
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr "Autenticazione"
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr "Impostazioni avanzate"
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Filtro"
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless-ng processerà solo la posta che rientra in TUTTI i filtri impostati."
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Azioni"
#: paperless_mail/admin.py:51
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 "L'azione che viene applicata alla email. Questa azione viene eseguita solo quando dei documenti vengono elaborati dalla email. Le email senza allegati vengono ignorate."
#: paperless_mail/admin.py:58
msgid "Metadata"
msgstr "Metadati"
#: paperless_mail/admin.py:60
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 "Assegna automaticamente i metadati ai documenti elaborati da questa regola. Se non assegni qui dei tag, tipi di documenti o corrispondenti, Paperless userà comunque le regole corrispondenti che hai configurato."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Email Paperless"
#: paperless_mail/models.py:11
msgid "mail account"
msgstr "account email"
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr "account email"
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr "Nessuna crittografia"
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr "Usa SSL"
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr "Usa STARTTLS"
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr "Server IMAP"
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr "Porta IMAP"
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "Di solito si usa 143 per STARTTLS o nessuna crittografia e 993 per SSL."
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr "Sicurezza IMAP"
#: paperless_mail/models.py:46
msgid "username"
msgstr "nome utente"
#: paperless_mail/models.py:50
msgid "password"
msgstr "password"
#: paperless_mail/models.py:54
msgid "character set"
msgstr "set di caratteri"
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr "Il set di caratteri da usare quando si comunica con il server di posta, come 'UTF-8' o 'US-ASCII'."
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "regola email"
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "regole email"
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "Elabora solo gli allegati."
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "Elabora tutti i file, inclusi gli allegati nel corpo."
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "Segna come letto, non elaborare le email lette"
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "Contrassegna la email, non elaborare le email elaborate."
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "Sposta in una cartella"
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Elimina"
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "Usa oggetto come titolo"
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "Usa il nome dell'allegato come titolo"
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "Non assegnare un corrispondente"
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "Usa indirizzo email"
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "Usa nome (o indirizzo email se non disponibile)"
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "Usa il corrispondente selezionato qui sotto"
#: paperless_mail/models.py:121
msgid "order"
msgstr "priorità"
#: paperless_mail/models.py:128
msgid "account"
msgstr "account"
#: paperless_mail/models.py:132
msgid "folder"
msgstr "cartella"
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr "Le sottocartelle devono essere separate da punti."
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "filtra da"
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "filtra oggetto"
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "filtra corpo"
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "filtra nome allegato"
#: paperless_mail/models.py:150
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "Elabora i documenti che corrispondono a questo nome. Puoi usare wildcard come *.pdf o *fattura*. Non fa differenza fra maiuscole e minuscole."
#: paperless_mail/models.py:156
msgid "maximum age"
msgstr "età massima"
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "Definito in giorni."
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "tipo di allegato"
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "Gli allegati in linea includono le immagini nel corpo, quindi è meglio combinare questa opzione con il filtro nome."
#: paperless_mail/models.py:169
msgid "action"
msgstr "azione"
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "parametro azione"
#: paperless_mail/models.py:177
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 "Parametro aggiuntivo per l'azione selezionata, ad esempio la cartella di destinazione per l'azione che sposta una cartella. Le sottocartelle devono essere separate da punti."
#: paperless_mail/models.py:184
msgid "assign title from"
msgstr "assegna tittolo da"
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "assegna questo tag"
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "assegna questo tipo di documento"
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "assegna corrispondente da"
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "assegna questo corrispondente"

View File

@@ -0,0 +1,642 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-28 12:40+0100\n"
"PO-Revision-Date: 2021-03-06 21:39\n"
"Last-Translator: \n"
"Language-Team: Latin\n"
"Language: la_LA\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-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: la-LA\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\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:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:109
msgid "name"
msgstr ""
#: documents/models.py:45
msgid "match"
msgstr ""
#: documents/models.py:49
msgid "matching algorithm"
msgstr ""
#: documents/models.py:55
msgid "is insensitive"
msgstr ""
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr ""
#: documents/models.py:75
msgid "correspondents"
msgstr ""
#: documents/models.py:81
msgid "color"
msgstr ""
#: documents/models.py:87
msgid "is inbox tag"
msgstr ""
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr ""
#: documents/models.py:94
msgid "tag"
msgstr ""
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr ""
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr ""
#: documents/models.py:102
msgid "document types"
msgstr ""
#: documents/models.py:110
msgid "Unencrypted"
msgstr ""
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr ""
#: documents/models.py:124
msgid "title"
msgstr ""
#: documents/models.py:137
msgid "content"
msgstr ""
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr ""
#: documents/models.py:144
msgid "mime type"
msgstr ""
#: documents/models.py:155
msgid "checksum"
msgstr ""
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr ""
#: documents/models.py:163
msgid "archive checksum"
msgstr ""
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr ""
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr ""
#: documents/models.py:176
msgid "modified"
msgstr ""
#: documents/models.py:180
msgid "storage type"
msgstr ""
#: documents/models.py:188
msgid "added"
msgstr ""
#: documents/models.py:192
msgid "filename"
msgstr ""
#: documents/models.py:198
msgid "Current filename in storage"
msgstr ""
#: documents/models.py:202
msgid "archive filename"
msgstr ""
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr ""
#: documents/models.py:212
msgid "archive serial number"
msgstr ""
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr ""
#: documents/models.py:223
msgid "document"
msgstr ""
#: documents/models.py:224
msgid "documents"
msgstr ""
#: documents/models.py:311
msgid "debug"
msgstr ""
#: documents/models.py:312
msgid "information"
msgstr ""
#: documents/models.py:313
msgid "warning"
msgstr ""
#: documents/models.py:314
msgid "error"
msgstr ""
#: documents/models.py:315
msgid "critical"
msgstr ""
#: documents/models.py:319
msgid "group"
msgstr ""
#: documents/models.py:322
msgid "message"
msgstr ""
#: documents/models.py:325
msgid "level"
msgstr ""
#: documents/models.py:332
msgid "log"
msgstr ""
#: documents/models.py:333
msgid "logs"
msgstr ""
#: documents/models.py:344 documents/models.py:394
msgid "saved view"
msgstr ""
#: documents/models.py:345
msgid "saved views"
msgstr ""
#: documents/models.py:348
msgid "user"
msgstr ""
#: documents/models.py:354
msgid "show on dashboard"
msgstr ""
#: documents/models.py:357
msgid "show in sidebar"
msgstr ""
#: documents/models.py:361
msgid "sort field"
msgstr ""
#: documents/models.py:364
msgid "sort reverse"
msgstr ""
#: documents/models.py:370
msgid "title contains"
msgstr ""
#: documents/models.py:371
msgid "content contains"
msgstr ""
#: documents/models.py:372
msgid "ASN is"
msgstr ""
#: documents/models.py:373
msgid "correspondent is"
msgstr ""
#: documents/models.py:374
msgid "document type is"
msgstr ""
#: documents/models.py:375
msgid "is in inbox"
msgstr ""
#: documents/models.py:376
msgid "has tag"
msgstr ""
#: documents/models.py:377
msgid "has any tag"
msgstr ""
#: documents/models.py:378
msgid "created before"
msgstr ""
#: documents/models.py:379
msgid "created after"
msgstr ""
#: documents/models.py:380
msgid "created year is"
msgstr ""
#: documents/models.py:381
msgid "created month is"
msgstr ""
#: documents/models.py:382
msgid "created day is"
msgstr ""
#: documents/models.py:383
msgid "added before"
msgstr ""
#: documents/models.py:384
msgid "added after"
msgstr ""
#: documents/models.py:385
msgid "modified before"
msgstr ""
#: documents/models.py:386
msgid "modified after"
msgstr ""
#: documents/models.py:387
msgid "does not have tag"
msgstr ""
#: documents/models.py:398
msgid "rule type"
msgstr ""
#: documents/models.py:402
msgid "value"
msgstr ""
#: documents/models.py:408
msgid "filter rule"
msgstr ""
#: documents/models.py:409
msgid "filter rules"
msgstr ""
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expresssion: %(error)s"
msgstr ""
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr ""
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
#: documents/templates/index.html:21
msgid "Paperless-ng is loading..."
msgstr ""
#: documents/templates/registration/logged_out.html:13
msgid "Paperless-ng signed out"
msgstr ""
#: documents/templates/registration/logged_out.html:41
msgid "You have been successfully logged out. Bye!"
msgstr ""
#: documents/templates/registration/logged_out.html:42
msgid "Sign in again"
msgstr ""
#: documents/templates/registration/login.html:13
msgid "Paperless-ng sign in"
msgstr ""
#: documents/templates/registration/login.html:42
msgid "Please sign in."
msgstr ""
#: documents/templates/registration/login.html:45
msgid "Your username and password didn't match. Please try again."
msgstr ""
#: documents/templates/registration/login.html:48
msgid "Username"
msgstr ""
#: documents/templates/registration/login.html:49
msgid "Password"
msgstr ""
#: documents/templates/registration/login.html:54
msgid "Sign in"
msgstr ""
#: paperless/settings.py:297
msgid "English (US)"
msgstr ""
#: paperless/settings.py:298
msgid "English (GB)"
msgstr ""
#: paperless/settings.py:299
msgid "German"
msgstr ""
#: paperless/settings.py:300
msgid "Dutch"
msgstr ""
#: paperless/settings.py:301
msgid "French"
msgstr ""
#: paperless/settings.py:302
msgid "Portuguese (Brazil)"
msgstr ""
#: paperless/settings.py:303
msgid "Italian"
msgstr ""
#: paperless/settings.py:304
msgid "Romanian"
msgstr ""
#: paperless/urls.py:118
msgid "Paperless-ng administration"
msgstr ""
#: paperless_mail/admin.py:25
msgid "Filter"
msgstr ""
#: paperless_mail/admin.py:27
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr ""
#: paperless_mail/admin.py:37
msgid "Actions"
msgstr ""
#: paperless_mail/admin.py:39
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:46
msgid "Metadata"
msgstr ""
#: paperless_mail/admin.py:48
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_mail/apps.py:9
msgid "Paperless mail"
msgstr ""
#: paperless_mail/models.py:11
msgid "mail account"
msgstr ""
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr ""
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr ""
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr ""
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr ""
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr ""
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr ""
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr ""
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr ""
#: paperless_mail/models.py:46
msgid "username"
msgstr ""
#: paperless_mail/models.py:50
msgid "password"
msgstr ""
#: paperless_mail/models.py:60
msgid "mail rule"
msgstr ""
#: paperless_mail/models.py:61
msgid "mail rules"
msgstr ""
#: paperless_mail/models.py:67
msgid "Only process attachments."
msgstr ""
#: paperless_mail/models.py:68
msgid "Process all files, including 'inline' attachments."
msgstr ""
#: paperless_mail/models.py:78
msgid "Mark as read, don't process read mails"
msgstr ""
#: paperless_mail/models.py:79
msgid "Flag the mail, don't process flagged mails"
msgstr ""
#: paperless_mail/models.py:80
msgid "Move to specified folder"
msgstr ""
#: paperless_mail/models.py:81
msgid "Delete"
msgstr ""
#: paperless_mail/models.py:88
msgid "Use subject as title"
msgstr ""
#: paperless_mail/models.py:89
msgid "Use attachment filename as title"
msgstr ""
#: paperless_mail/models.py:99
msgid "Do not assign a correspondent"
msgstr ""
#: paperless_mail/models.py:101
msgid "Use mail address"
msgstr ""
#: paperless_mail/models.py:103
msgid "Use name (or mail address if not available)"
msgstr ""
#: paperless_mail/models.py:105
msgid "Use correspondent selected below"
msgstr ""
#: paperless_mail/models.py:113
msgid "order"
msgstr ""
#: paperless_mail/models.py:120
msgid "account"
msgstr ""
#: paperless_mail/models.py:124
msgid "folder"
msgstr ""
#: paperless_mail/models.py:128
msgid "filter from"
msgstr ""
#: paperless_mail/models.py:131
msgid "filter subject"
msgstr ""
#: paperless_mail/models.py:134
msgid "filter body"
msgstr ""
#: paperless_mail/models.py:138
msgid "filter attachment filename"
msgstr ""
#: paperless_mail/models.py:140
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:146
msgid "maximum age"
msgstr ""
#: paperless_mail/models.py:148
msgid "Specified in days."
msgstr ""
#: paperless_mail/models.py:151
msgid "attachment type"
msgstr ""
#: paperless_mail/models.py:154
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr ""
#: paperless_mail/models.py:159
msgid "action"
msgstr ""
#: paperless_mail/models.py:165
msgid "action parameter"
msgstr ""
#: paperless_mail/models.py:167
msgid "Additional parameter for the action selected above, i.e., the target folder of the move to folder action."
msgstr ""
#: paperless_mail/models.py:173
msgid "assign title from"
msgstr ""
#: paperless_mail/models.py:183
msgid "assign this tag"
msgstr ""
#: paperless_mail/models.py:191
msgid "assign this document type"
msgstr ""
#: paperless_mail/models.py:195
msgid "assign correspondent from"
msgstr ""
#: paperless_mail/models.py:205
msgid "assign this correspondent"
msgstr ""

View File

@@ -0,0 +1,698 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-07-16 14:22\n"
"Last-Translator: \n"
"Language-Team: Luxembourgish\n"
"Language: lb_LU\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-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: lb\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Dokumenter"
#: documents/models.py:32
msgid "Any word"
msgstr "Iergendee Wuert"
#: documents/models.py:33
msgid "All words"
msgstr "All d'Wierder"
#: documents/models.py:34
msgid "Exact match"
msgstr "Exakten Treffer"
#: documents/models.py:35
msgid "Regular expression"
msgstr "Regulären Ausdrock"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Ongenaut Wuert"
#: documents/models.py:37
msgid "Automatic"
msgstr "Automatesch"
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "Numm"
#: documents/models.py:45
msgid "match"
msgstr "Zouweisungsmuster"
#: documents/models.py:49
msgid "matching algorithm"
msgstr "Zouweisungsalgorithmus"
#: documents/models.py:55
msgid "is insensitive"
msgstr "Grouss-/Klengschreiwung ignoréieren"
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "Korrespondent"
#: documents/models.py:75
msgid "correspondents"
msgstr "Korrespondenten"
#: documents/models.py:81
msgid "color"
msgstr "Faarf"
#: documents/models.py:87
msgid "is inbox tag"
msgstr "Postaganks-Etikett"
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Dës Etikett als Postaganks-Etikett markéieren: All nei importéiert Dokumenter kréien ëmmer dës Etikett zougewisen."
#: documents/models.py:94
msgid "tag"
msgstr "Etikett"
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "Etiketten"
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "Dokumententyp"
#: documents/models.py:102
msgid "document types"
msgstr "Dokumententypen"
#: documents/models.py:110
msgid "Unencrypted"
msgstr "Onverschlësselt"
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "Verschlësselt mat GNU Privacy Guard"
#: documents/models.py:124
msgid "title"
msgstr "Titel"
#: documents/models.py:137
msgid "content"
msgstr "Inhalt"
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "De réien Textinhalt vum Dokument. Dëst Feld gëtt primär fir d'Sich benotzt."
#: documents/models.py:144
msgid "mime type"
msgstr "MIME-Typ"
#: documents/models.py:155
msgid "checksum"
msgstr "Préifzomm"
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "D'Préifzomm vum Original-Dokument."
#: documents/models.py:163
msgid "archive checksum"
msgstr "Archiv-Préifzomm"
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "D'Préifzomm vum archivéierten Dokument."
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "erstallt"
#: documents/models.py:176
msgid "modified"
msgstr "verännert"
#: documents/models.py:180
msgid "storage type"
msgstr "Späichertyp"
#: documents/models.py:188
msgid "added"
msgstr "derbäigesat"
#: documents/models.py:192
msgid "filename"
msgstr "Fichiersnumm"
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "Aktuellen Dateinumm am Späicher"
#: documents/models.py:202
msgid "archive filename"
msgstr "Archiv-Dateinumm"
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr "Aktuellen Dateinumm am Archiv"
#: documents/models.py:212
msgid "archive serial number"
msgstr "Archiv-Seriennummer"
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr "D'Positioun vun dësem Dokument am physeschen Dokumentenarchiv."
#: documents/models.py:223
msgid "document"
msgstr "Dokument"
#: documents/models.py:224
msgid "documents"
msgstr "Dokumenter"
#: documents/models.py:311
msgid "debug"
msgstr "Fehlersiich"
#: documents/models.py:312
msgid "information"
msgstr "Informatioun"
#: documents/models.py:313
msgid "warning"
msgstr "Warnung"
#: documents/models.py:314
msgid "error"
msgstr "Feeler"
#: documents/models.py:315
msgid "critical"
msgstr "kritesch"
#: documents/models.py:319
msgid "group"
msgstr "Grupp"
#: documents/models.py:322
msgid "message"
msgstr "Message"
#: documents/models.py:325
msgid "level"
msgstr "Niveau"
#: documents/models.py:332
msgid "log"
msgstr "Protokoll"
#: documents/models.py:333
msgid "logs"
msgstr "Protokoller"
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "Gespäichert Usiicht"
#: documents/models.py:345
msgid "saved views"
msgstr "Gespäichert Usiichten"
#: documents/models.py:348
msgid "user"
msgstr "Benotzer"
#: documents/models.py:354
msgid "show on dashboard"
msgstr "Op der Startsäit uweisen"
#: documents/models.py:357
msgid "show in sidebar"
msgstr "An der Säiteleescht uweisen"
#: documents/models.py:361
msgid "sort field"
msgstr "Zortéierfeld"
#: documents/models.py:367
msgid "sort reverse"
msgstr "ëmgedréint zortéieren"
#: documents/models.py:373
msgid "title contains"
msgstr "Titel enthält"
#: documents/models.py:374
msgid "content contains"
msgstr "Inhalt enthält"
#: documents/models.py:375
msgid "ASN is"
msgstr "ASN ass"
#: documents/models.py:376
msgid "correspondent is"
msgstr "Korrespondent ass"
#: documents/models.py:377
msgid "document type is"
msgstr "Dokumententyp ass"
#: documents/models.py:378
msgid "is in inbox"
msgstr "ass am Postagank"
#: documents/models.py:379
msgid "has tag"
msgstr "huet Etikett"
#: documents/models.py:380
msgid "has any tag"
msgstr "huet iergendeng Etikett"
#: documents/models.py:381
msgid "created before"
msgstr "erstallt virun"
#: documents/models.py:382
msgid "created after"
msgstr "erstallt no"
#: documents/models.py:383
msgid "created year is"
msgstr "Erstellungsjoer ass"
#: documents/models.py:384
msgid "created month is"
msgstr "Erstellungsmount ass"
#: documents/models.py:385
msgid "created day is"
msgstr "Erstellungsdag ass"
#: documents/models.py:386
msgid "added before"
msgstr "dobäigesat virun"
#: documents/models.py:387
msgid "added after"
msgstr "dobäigesat no"
#: documents/models.py:388
msgid "modified before"
msgstr "verännert virun"
#: documents/models.py:389
msgid "modified after"
msgstr "verännert no"
#: documents/models.py:390
msgid "does not have tag"
msgstr "huet dës Etikett net"
#: documents/models.py:391
msgid "does not have ASN"
msgstr "huet keng ASN"
#: documents/models.py:392
msgid "title or content contains"
msgstr "Titel oder Inhalt enthalen"
#: documents/models.py:393
msgid "fulltext query"
msgstr "Volltextsich"
#: documents/models.py:394
msgid "more like this"
msgstr "ähnlech Dokumenter"
#: documents/models.py:405
msgid "rule type"
msgstr "Reegeltyp"
#: documents/models.py:409
msgid "value"
msgstr "Wäert"
#: documents/models.py:415
msgid "filter rule"
msgstr "Filterreegel"
#: documents/models.py:416
msgid "filter rules"
msgstr "Filterreegelen"
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "Ongëltege regulären Ausdrock: %(error)s"
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr "Ongëlteg Faarf."
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "Fichierstyp %(type)s net ënnerstëtzt"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "Paperless-ng gëtt gelueden..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "Paperless-ng ofgemellt"
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "Dir hutt Iech erfollegräich ofgemellt. Bis geschwënn!"
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "Nees umellen"
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "Umeldung bei Paperless-ng"
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "W. e. g. umellen."
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr "Äre Benotzernumm a Passwuert stëmmen net iwwereneen. Probéiert w. e. g. nach emol."
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "Benotzernumm"
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "Passwuert"
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "Umellen"
#: paperless/settings.py:303
msgid "English (US)"
msgstr "Englesch (USA)"
#: paperless/settings.py:304
msgid "English (GB)"
msgstr "Englesch (GB)"
#: paperless/settings.py:305
msgid "German"
msgstr "Däitsch"
#: paperless/settings.py:306
msgid "Dutch"
msgstr "Hollännesch"
#: paperless/settings.py:307
msgid "French"
msgstr "Franséisch"
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr "Portugisesch (Brasilien)"
#: paperless/settings.py:309
msgid "Portuguese"
msgstr "Portugisesch"
#: paperless/settings.py:310
msgid "Italian"
msgstr "Italienesch"
#: paperless/settings.py:311
msgid "Romanian"
msgstr "Rumänesch"
#: paperless/settings.py:312
msgid "Russian"
msgstr "Russesch"
#: paperless/settings.py:313
msgid "Spanish"
msgstr "Spuenesch"
#: paperless/settings.py:314
msgid "Polish"
msgstr "Polnesch"
#: paperless/settings.py:315
msgid "Swedish"
msgstr "Schwedesch"
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "Paperless-ng-Administratioun"
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr "Authentifizéierung"
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr "Erweidert Astellungen"
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Filter"
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless wäert nëmmen E-Maile veraarbechten, fir déi all déi hei definéiert Filteren zoutreffen."
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Aktiounen"
#: paperless_mail/admin.py:51
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 "D'Aktioun, déi op E-Mailen applizéiert sill ginn. Dës Aktioun gëtt just ausgefouert, wann Dokumenter aus der E-Mail veraarbecht goufen. E-Mailen ouni Unhank bleiwe komplett onberéiert."
#: paperless_mail/admin.py:58
msgid "Metadata"
msgstr "Metadaten"
#: paperless_mail/admin.py:60
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 "Den Dokumenter, déi iwwer dës Reegel veraarbecht ginn, automatesch Metadaten zouweisen. Wann hei keng Etiketten, Typen oder Korrespondenten zougewise ginn, veraarbecht Paperless-ng trotzdeem all zoutreffend Reegelen déi definéiert sinn."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Paperless E-Mail"
#: paperless_mail/models.py:11
msgid "mail account"
msgstr "Mailkont"
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr "Mailkonten"
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr "Keng Verschlësselung"
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr "SSL benotzen"
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr "STARTTLS benotzen"
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr "IMAP-Server"
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr "IMAP-Port"
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "Dëst ass normalerweis 143 fir onverschësselt oder STARTTLS-Connectiounen an 993 fir SSL-Connectiounen."
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr "IMAP-Sécherheet"
#: paperless_mail/models.py:46
msgid "username"
msgstr "Benotzernumm"
#: paperless_mail/models.py:50
msgid "password"
msgstr "Passwuert"
#: paperless_mail/models.py:54
msgid "character set"
msgstr "Zeechesaz"
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr "Den Zeechesaz dee benotzt gëtt wa mam Mailserver kommunizéiert gëtt, wéi beispillsweis 'UTF-8' oder 'US-ASCII'."
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "E-Mail-Reegel"
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "E-Mail-Reegelen"
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "Just Unhäng veraarbechten."
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "All d'Fichiere veraarbechten, inklusiv \"inline\"-Unhäng."
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "Als gelies markéieren, gelies Mailen net traitéieren"
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "Als wichteg markéieren, markéiert E-Mailen net veraarbechten"
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "An e virdefinéierten Dossier réckelen"
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Läschen"
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "Sujet als TItel notzen"
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "Numm vum Dateiunhank als Titel benotzen"
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "Kee Korrespondent zouweisen"
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "E-Mail-Adress benotzen"
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "Numm benotzen (oder E-Mail-Adress falls den Numm net disponibel ass)"
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "Korrespondent benotzen deen hei drënner ausgewielt ass"
#: paperless_mail/models.py:121
msgid "order"
msgstr "Reiefolleg"
#: paperless_mail/models.py:128
msgid "account"
msgstr "Kont"
#: paperless_mail/models.py:132
msgid "folder"
msgstr "Dossier"
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr "Ënnerdossiere mussen duerch Punkte getrennt ginn."
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "Ofsenderfilter"
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "Sujets-Filter"
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "Contenu-Filter"
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "Filter fir den Numm vum Unhank"
#: paperless_mail/models.py:150
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "Just Dokumenter traitéieren, déi exakt dësen Dateinumm hunn (falls definéiert). Platzhalter wéi *.pdf oder *invoice* sinn erlaabt. D'Grouss-/Klengschreiwung gëtt ignoréiert."
#: paperless_mail/models.py:156
msgid "maximum age"
msgstr "Maximalen Alter"
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "An Deeg."
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "Fichierstyp vum Unhank"
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "\"Inline\"-Unhänk schléissen och agebonne Biller mat an, dofir sollt dës Astellung mat engem Filter fir den Numm vum Unhank kombinéiert ginn."
#: paperless_mail/models.py:169
msgid "action"
msgstr "Aktioun"
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "Parameter fir Aktioun"
#: paperless_mail/models.py:177
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 "Zousätzleche Parameter fir d'Aktioun déi hei driwwer ausgewielt ass, zum Beispill den Numm vum Zildossier fir d'Aktioun \"An e virdefinéierten Dossier réckelen\". Ënnerdossiere musse mat Punkte getrennt ginn."
#: paperless_mail/models.py:184
msgid "assign title from"
msgstr "Titel zouweisen aus"
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "dës Etikett zouweisen"
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "Dësen Dokumententyp zouweisen"
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "Korrespondent zouweisen aus"
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "Dëse Korrespondent zouweisen"

View File

@@ -1,26 +1,21 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Ben <bzweekhorst@gmail.com>, 2021
# Jo Vandeginste <jo.vandeginste@gmail.com>, 2021
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-10 21:41+0000\n"
"PO-Revision-Date: 2020-12-30 19:27+0000\n"
"Last-Translator: Jo Vandeginste <jo.vandeginste@gmail.com>, 2021\n"
"Language-Team: Dutch (Netherlands) (https://www.transifex.com/paperless/teams/115905/nl_NL/)\n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-05-22 10:12\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: nl_NL\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: paperless-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: nl\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
@@ -28,7 +23,7 @@ msgstr "Documenten"
#: documents/models.py:32
msgid "Any word"
msgstr "Eender welk woord"
msgstr "Elk woord"
#: documents/models.py:33
msgid "All words"
@@ -50,8 +45,8 @@ msgstr "Gelijkaardig woord"
msgid "Automatic"
msgstr "Automatisch"
#: documents/models.py:41 documents/models.py:354 paperless_mail/models.py:25
#: paperless_mail/models.py:109
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "naam"
@@ -67,383 +62,443 @@ msgstr "Algoritme voor het bepalen van de overeenkomst"
msgid "is insensitive"
msgstr "is niet hoofdlettergevoelig"
#: documents/models.py:80 documents/models.py:140
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "correspondent"
#: documents/models.py:81
#: documents/models.py:75
msgid "correspondents"
msgstr "correspondenten"
#: documents/models.py:103
#: documents/models.py:81
msgid "color"
msgstr "Kleur"
#: documents/models.py:107
#: documents/models.py:87
msgid "is inbox tag"
msgstr "is \"Postvak in\"-etiket"
msgstr "is \"Postvak in\"-label"
#: documents/models.py:109
msgid ""
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
"with inbox tags."
msgstr ""
"Markeer dit etiket als een \"Postvak in\"-etiket: alle nieuw verwerkte "
"documenten krijgen de \"Postvak in\"-etiketten."
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Markeert dit label als een \"Postvak in\"-label: alle nieuw verwerkte documenten krijgen de \"Postvak in\"-labels."
#: documents/models.py:114
#: documents/models.py:94
msgid "tag"
msgstr "etiket"
msgstr "label"
#: documents/models.py:115 documents/models.py:171
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "etiketten"
msgstr "labels"
#: documents/models.py:121 documents/models.py:153
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "documenttype"
#: documents/models.py:122
#: documents/models.py:102
msgid "document types"
msgstr "documenttypen"
#: documents/models.py:130
#: documents/models.py:110
msgid "Unencrypted"
msgstr "Niet versleuteld"
#: documents/models.py:131
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "Versleuteld met GNU Privacy Guard"
#: documents/models.py:144
#: documents/models.py:124
msgid "title"
msgstr "titel"
#: documents/models.py:157
#: documents/models.py:137
msgid "content"
msgstr "inhoud"
#: documents/models.py:159
msgid ""
"The raw, text-only data of the document. This field is primarily used for "
"searching."
msgstr ""
"De onbewerkte gegevens van het document. Dit veld wordt voornamelijk "
"gebruikt om te zoeken."
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "De onbewerkte gegevens van het document. Dit veld wordt voornamelijk gebruikt om te zoeken."
#: documents/models.py:164
#: documents/models.py:144
msgid "mime type"
msgstr "mimetype"
#: documents/models.py:175
#: documents/models.py:155
msgid "checksum"
msgstr "checksum"
#: documents/models.py:179
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "Het controlecijfer van het originele document."
msgstr "De checksum van het oorspronkelijke document."
#: documents/models.py:183
#: documents/models.py:163
msgid "archive checksum"
msgstr "archief checksum"
#: documents/models.py:188
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "De checksum van het gearchiveerde document."
#: documents/models.py:192 documents/models.py:332
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "aangemaakt"
#: documents/models.py:196
#: documents/models.py:176
msgid "modified"
msgstr "gewijzigd"
#: documents/models.py:200
#: documents/models.py:180
msgid "storage type"
msgstr "type opslag"
#: documents/models.py:208
#: documents/models.py:188
msgid "added"
msgstr "toegevoegd"
#: documents/models.py:212
#: documents/models.py:192
msgid "filename"
msgstr "bestandsnaam"
#: documents/models.py:217
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "Huidige bestandsnaam in opslag"
#: documents/models.py:221
#: documents/models.py:202
msgid "archive filename"
msgstr "Bestandsnaam in archief"
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr "Huidige bestandsnaam in archief"
#: documents/models.py:212
msgid "archive serial number"
msgstr "serienummer in archief"
#: documents/models.py:226
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr "De positie van dit document in je fysieke documentenarchief."
#: documents/models.py:232
#: documents/models.py:223
msgid "document"
msgstr "document"
#: documents/models.py:233
#: documents/models.py:224
msgid "documents"
msgstr "documenten"
#: documents/models.py:315
#: documents/models.py:311
msgid "debug"
msgstr "debug"
#: documents/models.py:316
#: documents/models.py:312
msgid "information"
msgstr "informatie"
#: documents/models.py:317
#: documents/models.py:313
msgid "warning"
msgstr "waarschuwing"
#: documents/models.py:318
#: documents/models.py:314
msgid "error"
msgstr "fout"
#: documents/models.py:319
#: documents/models.py:315
msgid "critical"
msgstr "kritisch"
#: documents/models.py:323
#: documents/models.py:319
msgid "group"
msgstr "groep"
#: documents/models.py:326
#: documents/models.py:322
msgid "message"
msgstr "bericht"
#: documents/models.py:329
#: documents/models.py:325
msgid "level"
msgstr "niveau"
#: documents/models.py:336
#: documents/models.py:332
msgid "log"
msgstr "bericht"
#: documents/models.py:337
#: documents/models.py:333
msgid "logs"
msgstr "berichten"
#: documents/models.py:348 documents/models.py:398
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "opgeslagen view"
#: documents/models.py:349
#: documents/models.py:345
msgid "saved views"
msgstr "opgeslagen views"
#: documents/models.py:352
#: documents/models.py:348
msgid "user"
msgstr "gebruiker"
#: documents/models.py:358
#: documents/models.py:354
msgid "show on dashboard"
msgstr "weergeven op dashboard"
#: documents/models.py:361
#: documents/models.py:357
msgid "show in sidebar"
msgstr "weergeven in zijbalk"
#: documents/models.py:365
#: documents/models.py:361
msgid "sort field"
msgstr "sorteerveld"
#: documents/models.py:368
#: documents/models.py:367
msgid "sort reverse"
msgstr "omgekeerd sorteren"
#: documents/models.py:374
#: documents/models.py:373
msgid "title contains"
msgstr "titel bevat"
#: documents/models.py:375
#: documents/models.py:374
msgid "content contains"
msgstr "inhoud bevat"
#: documents/models.py:376
#: documents/models.py:375
msgid "ASN is"
msgstr "ASN is"
#: documents/models.py:377
#: documents/models.py:376
msgid "correspondent is"
msgstr "correspondent is"
#: documents/models.py:378
#: documents/models.py:377
msgid "document type is"
msgstr "documenttype is"
#: documents/models.py:379
#: documents/models.py:378
msgid "is in inbox"
msgstr "zit in \"Postvak in\""
#: documents/models.py:380
#: documents/models.py:379
msgid "has tag"
msgstr "heeft etiket"
msgstr "heeft label"
#: documents/models.py:380
msgid "has any tag"
msgstr "heeft één van de labels"
#: documents/models.py:381
msgid "has any tag"
msgstr "heeft één van de etiketten"
#: documents/models.py:382
msgid "created before"
msgstr "aangemaakt voor"
#: documents/models.py:383
#: documents/models.py:382
msgid "created after"
msgstr "aangemaakt na"
#: documents/models.py:384
#: documents/models.py:383
msgid "created year is"
msgstr "aangemaakt jaar is"
#: documents/models.py:385
#: documents/models.py:384
msgid "created month is"
msgstr "aangemaakte maand is"
#: documents/models.py:386
#: documents/models.py:385
msgid "created day is"
msgstr "aangemaakte dag is"
#: documents/models.py:387
#: documents/models.py:386
msgid "added before"
msgstr "toegevoegd voor"
#: documents/models.py:388
#: documents/models.py:387
msgid "added after"
msgstr "toegevoegd na"
#: documents/models.py:389
#: documents/models.py:388
msgid "modified before"
msgstr "gewijzigd voor"
#: documents/models.py:390
#: documents/models.py:389
msgid "modified after"
msgstr "gewijzigd na"
#: documents/models.py:391
#: documents/models.py:390
msgid "does not have tag"
msgstr "heeft geen etiket"
msgstr "heeft geen label"
#: documents/models.py:402
#: documents/models.py:391
msgid "does not have ASN"
msgstr "heeft geen ASN"
#: documents/models.py:392
msgid "title or content contains"
msgstr "titel of inhoud bevat"
#: documents/models.py:393
msgid "fulltext query"
msgstr "inhoud doorzoeken"
#: documents/models.py:394
msgid "more like this"
msgstr "meer zoals dit"
#: documents/models.py:405
msgid "rule type"
msgstr "type regel"
#: documents/models.py:406
#: documents/models.py:409
msgid "value"
msgstr "waarde"
#: documents/models.py:412
#: documents/models.py:415
msgid "filter rule"
msgstr "filterregel"
#: documents/models.py:413
#: documents/models.py:416
msgid "filter rules"
msgstr "filterregels"
#: documents/templates/index.html:20
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "Ongeldige reguliere expressie: %(error)s"
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr "Ongeldig kleur."
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "Bestandstype %(type)s niet ondersteund"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "Paperless-ng is aan het laden..."
#: documents/templates/registration/logged_out.html:13
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "Paperless-ng - afmelden"
#: documents/templates/registration/logged_out.html:41
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "Je bent nu afgemeld. Tot later!"
#: documents/templates/registration/logged_out.html:42
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "Meld je opnieuw aan"
#: documents/templates/registration/login.html:13
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "Paperless-ng - aanmelden"
#: documents/templates/registration/login.html:42
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "Gelieve aan te melden."
#: documents/templates/registration/login.html:45
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr "Je gebruikersnaam en wachtwoord komen niet overeen. Probeer opnieuw."
#: documents/templates/registration/login.html:48
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "Gebruikersnaam"
#: documents/templates/registration/login.html:49
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "Wachtwoord"
#: documents/templates/registration/login.html:54
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "Aanmelden"
#: paperless/settings.py:268
msgid "English"
msgstr "Engels"
#: paperless/settings.py:303
msgid "English (US)"
msgstr "Engels (US)"
#: paperless/settings.py:269
#: paperless/settings.py:304
msgid "English (GB)"
msgstr "Engels (Brits)"
#: paperless/settings.py:305
msgid "German"
msgstr "Duits"
#: paperless/settings.py:270
#: paperless/settings.py:306
msgid "Dutch"
msgstr "Nederlands"
#: paperless/settings.py:271
#: paperless/settings.py:307
msgid "French"
msgstr "Frans"
#: paperless/urls.py:108
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr "Portugees (Brazilië)"
#: paperless/settings.py:309
msgid "Portuguese"
msgstr "Portugees"
#: paperless/settings.py:310
msgid "Italian"
msgstr "Italiaans"
#: paperless/settings.py:311
msgid "Romanian"
msgstr "Roemeens"
#: paperless/settings.py:312
msgid "Russian"
msgstr "Russisch"
#: paperless/settings.py:313
msgid "Spanish"
msgstr "Spaans"
#: paperless/settings.py:314
msgid "Polish"
msgstr "Pools"
#: paperless/settings.py:315
msgid "Swedish"
msgstr "Zweeds"
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "Paperless-ng administratie"
#: paperless_mail/admin.py:25
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr "Authenticatie"
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr "Geavanceerde instellingen"
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Filter"
#: paperless_mail/admin.py:27
msgid ""
"Paperless will only process mails that match ALL of the filters given below."
msgstr ""
"Paperless verwerkt alleen e-mails die voldoen aan ALLE onderstaande filters."
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless verwerkt alleen e-mails die voldoen aan ALLE onderstaande filters."
#: paperless_mail/admin.py:37
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Acties"
#: paperless_mail/admin.py:39
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 ""
"De actie die wordt toegepast op de mail. Deze actie wordt alleen uitgevoerd "
"wanneer documenten verwerkt werden uit de mail. Mails zonder bijlage blijven"
" onaangeroerd."
#: paperless_mail/admin.py:51
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 "De actie die wordt toegepast op de mail. Deze actie wordt alleen uitgevoerd wanneer documenten verwerkt werden uit de mail. Mails zonder bijlage blijven onaangeroerd."
#: paperless_mail/admin.py:46
#: paperless_mail/admin.py:58
msgid "Metadata"
msgstr "Metadata"
#: paperless_mail/admin.py:48
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 ""
"Automatisch metadata toewijzen aan documenten vanuit deze regel. Indien je "
"geen etiketten, documenttypes of correspondenten toewijst, zal Paperless nog"
" steeds alle regels verwerken die je hebt gedefinieerd."
#: paperless_mail/admin.py:60
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 "Automatisch metadata toewijzen aan documenten vanuit deze regel. Indien je geen labels, documenttypes of correspondenten toewijst, zal Paperless nog steeds alle regels verwerken die je hebt gedefinieerd."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
@@ -478,12 +533,8 @@ msgid "IMAP port"
msgstr "IMAP-poort"
#: paperless_mail/models.py:36
msgid ""
"This is usually 143 for unencrypted and STARTTLS connections, and 993 for "
"SSL connections."
msgstr ""
"Dit is gewoonlijk 143 voor onversleutelde of STARTTLS verbindingen, en 993 "
"voor SSL verbindingen."
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "Dit is gewoonlijk 143 voor onversleutelde of STARTTLS verbindingen, en 993 voor SSL verbindingen."
#: paperless_mail/models.py:40
msgid "IMAP security"
@@ -497,151 +548,151 @@ msgstr "gebruikersnaam"
msgid "password"
msgstr "wachtwoord"
#: paperless_mail/models.py:60
#: paperless_mail/models.py:54
msgid "character set"
msgstr "Tekenset"
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr "Tekenset die gebruikt moet worden bij communicatie met de mailserver, zoals 'UTF-8' of 'US-ASCII'."
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "email-regel"
#: paperless_mail/models.py:61
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "email-regels"
#: paperless_mail/models.py:67
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "Alleen bijlagen verwerken"
#: paperless_mail/models.py:68
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "Verwerk alle bestanden, inclusief 'inline' bijlagen."
#: paperless_mail/models.py:78
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "Markeer als gelezen, verwerk geen gelezen mails"
#: paperless_mail/models.py:79
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "Markeer de mail, verwerk geen mails met markering"
#: paperless_mail/models.py:80
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "Verplaats naar gegeven map"
#: paperless_mail/models.py:81
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Verwijder"
#: paperless_mail/models.py:88
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "Gebruik onderwerp als titel"
#: paperless_mail/models.py:89
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "Gebruik naam van bijlage als titel"
#: paperless_mail/models.py:99
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "Wijs geen correspondent toe"
#: paperless_mail/models.py:101
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "Gebruik het email-adres"
#: paperless_mail/models.py:103
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "Gebruik de naam, en anders het email-adres"
#: paperless_mail/models.py:105
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "Gebruik de hieronder aangeduide correspondent"
#: paperless_mail/models.py:113
#: paperless_mail/models.py:121
msgid "order"
msgstr "volgorde"
#: paperless_mail/models.py:120
#: paperless_mail/models.py:128
msgid "account"
msgstr "account"
#: paperless_mail/models.py:124
#: paperless_mail/models.py:132
msgid "folder"
msgstr "map"
#: paperless_mail/models.py:128
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr "Submappen moeten gescheiden worden door punten."
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "filter afzender"
#: paperless_mail/models.py:131
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "filter onderwerp"
#: paperless_mail/models.py:134
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "filter inhoud"
#: paperless_mail/models.py:138
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "Filter bestandsnaam van bijlage"
#: paperless_mail/models.py:140
msgid ""
"Only consume documents which entirely match this filename if specified. "
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr ""
"Alleen documenten verwerken die volledig overeenkomen, indien aangegeven. Je"
" kunt jokertekens gebruiken, zoals *.pdf of *factuur*. Dit is niet "
"hoofdlettergevoelig."
#: paperless_mail/models.py:150
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "Alleen documenten verwerken die volledig overeenkomen, indien aangegeven. Je kunt jokertekens gebruiken, zoals *.pdf of *factuur*. Dit is niet hoofdlettergevoelig."
#: paperless_mail/models.py:146
#: paperless_mail/models.py:156
msgid "maximum age"
msgstr "Maximale leeftijd"
#: paperless_mail/models.py:148
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "Aangegeven in dagen"
#: paperless_mail/models.py:151
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "Type bijlage"
#: paperless_mail/models.py:154
msgid ""
"Inline attachments include embedded images, so it's best to combine this "
"option with a filename filter."
msgstr ""
"\"Inline\" bijlagen bevatten vaak ook afbeeldingen. In dit geval valt het "
"aan te raden om ook een filter voor de bestandsnaam op te geven."
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "\"Inline\" bijlagen bevatten vaak ook afbeeldingen. In dit geval valt het aan te raden om ook een filter voor de bestandsnaam op te geven."
#: paperless_mail/models.py:159
#: paperless_mail/models.py:169
msgid "action"
msgstr "actie"
#: paperless_mail/models.py:165
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "actie parameters"
#: paperless_mail/models.py:167
msgid ""
"Additional parameter for the action selected above, i.e., the target folder "
"of the move to folder action."
msgstr ""
"Extra parameters voor de hierboven gekozen actie, met andere woorden: de "
"bestemmingsmap voor de verplaats-actie."
#: paperless_mail/models.py:177
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 "Extra parameter voor de hierboven geselecteerde actie, bijvoorbeeld: de doelmap voor de \"verplaats naar map\"-actie. Submappen moeten gescheiden worden door punten."
#: paperless_mail/models.py:173
#: paperless_mail/models.py:184
msgid "assign title from"
msgstr "wijs titel toe van"
#: paperless_mail/models.py:183
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "wijs dit etiket toe"
#: paperless_mail/models.py:191
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "wijs dit documenttype toe"
#: paperless_mail/models.py:195
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "wijs correspondent toe van"
#: paperless_mail/models.py:205
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "wijs deze correspondent toe"

View File

@@ -0,0 +1,698 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-08-07 13:02\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Language: pl_PL\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==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
"X-Crowdin-Project: paperless-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: pl\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Dokumenty"
#: documents/models.py:32
msgid "Any word"
msgstr "Dowolne słowo"
#: documents/models.py:33
msgid "All words"
msgstr "Wszystkie słowa"
#: documents/models.py:34
msgid "Exact match"
msgstr "Dokładne dopasowanie"
#: documents/models.py:35
msgid "Regular expression"
msgstr "Wyrażenie regularne"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Dopasowanie rozmyte"
#: documents/models.py:37
msgid "Automatic"
msgstr "Automatyczny"
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "nazwa"
#: documents/models.py:45
msgid "match"
msgstr "dopasowanie"
#: documents/models.py:49
msgid "matching algorithm"
msgstr "algorytm dopasowania"
#: documents/models.py:55
msgid "is insensitive"
msgstr "bez rozróżniania wielkości liter"
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "korespondent"
#: documents/models.py:75
msgid "correspondents"
msgstr "korespondenci"
#: documents/models.py:81
msgid "color"
msgstr "kolor"
#: documents/models.py:87
msgid "is inbox tag"
msgstr "jest tagiem skrzynki odbiorczej"
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Zaznacza ten tag jako tag skrzynki odbiorczej: Wszystkie nowo przetworzone dokumenty będą oznaczone tagami skrzynki odbiorczej."
#: documents/models.py:94
msgid "tag"
msgstr "tag"
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "tagi"
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "typ dokumentu"
#: documents/models.py:102
msgid "document types"
msgstr "typy dokumentów"
#: documents/models.py:110
msgid "Unencrypted"
msgstr "Niezaszyfrowane"
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "Zaszyfrowane przy użyciu GNU Privacy Guard"
#: documents/models.py:124
msgid "title"
msgstr "tytuł"
#: documents/models.py:137
msgid "content"
msgstr "zawartość"
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "Surowe, tekstowe dane dokumentu. To pole jest używane głównie do wyszukiwania."
#: documents/models.py:144
msgid "mime type"
msgstr "typ mime"
#: documents/models.py:155
msgid "checksum"
msgstr "suma kontrolna"
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "Suma kontrolna oryginalnego dokumentu."
#: documents/models.py:163
msgid "archive checksum"
msgstr "suma kontrolna archiwum"
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "Suma kontrolna zarchiwizowanego dokumentu."
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "utworzono"
#: documents/models.py:176
msgid "modified"
msgstr "zmodyfikowano"
#: documents/models.py:180
msgid "storage type"
msgstr "typ przechowywania"
#: documents/models.py:188
msgid "added"
msgstr "dodano"
#: documents/models.py:192
msgid "filename"
msgstr "nazwa pliku"
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "Aktualna nazwa pliku w pamięci"
#: documents/models.py:202
msgid "archive filename"
msgstr "nazwa pliku archiwum"
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr "Aktualna nazwa pliku archiwum w pamięci"
#: documents/models.py:212
msgid "archive serial number"
msgstr "numer seryjny archiwum"
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr "Pozycja tego dokumentu w archiwum dokumentów fizycznych."
#: documents/models.py:223
msgid "document"
msgstr "dokument"
#: documents/models.py:224
msgid "documents"
msgstr "dokumenty"
#: documents/models.py:311
msgid "debug"
msgstr "debug"
#: documents/models.py:312
msgid "information"
msgstr "informacja"
#: documents/models.py:313
msgid "warning"
msgstr "ostrzeżenie"
#: documents/models.py:314
msgid "error"
msgstr "błąd"
#: documents/models.py:315
msgid "critical"
msgstr "krytyczne"
#: documents/models.py:319
msgid "group"
msgstr "grupa"
#: documents/models.py:322
msgid "message"
msgstr "wiadomość"
#: documents/models.py:325
msgid "level"
msgstr "poziom"
#: documents/models.py:332
msgid "log"
msgstr "log"
#: documents/models.py:333
msgid "logs"
msgstr "logi"
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "zapisany widok"
#: documents/models.py:345
msgid "saved views"
msgstr "zapisane widoki"
#: documents/models.py:348
msgid "user"
msgstr "użytkownik"
#: documents/models.py:354
msgid "show on dashboard"
msgstr "pokaż na pulpicie"
#: documents/models.py:357
msgid "show in sidebar"
msgstr "pokaż na pasku bocznym"
#: documents/models.py:361
msgid "sort field"
msgstr "pole sortowania"
#: documents/models.py:367
msgid "sort reverse"
msgstr "sortuj malejąco"
#: documents/models.py:373
msgid "title contains"
msgstr "tytuł zawiera"
#: documents/models.py:374
msgid "content contains"
msgstr "zawartość zawiera"
#: documents/models.py:375
msgid "ASN is"
msgstr "numer archiwum jest"
#: documents/models.py:376
msgid "correspondent is"
msgstr "korespondentem jest"
#: documents/models.py:377
msgid "document type is"
msgstr "typ dokumentu jest"
#: documents/models.py:378
msgid "is in inbox"
msgstr "jest w skrzynce odbiorczej"
#: documents/models.py:379
msgid "has tag"
msgstr "ma tag"
#: documents/models.py:380
msgid "has any tag"
msgstr "ma dowolny tag"
#: documents/models.py:381
msgid "created before"
msgstr "utworzony przed"
#: documents/models.py:382
msgid "created after"
msgstr "utworzony po"
#: documents/models.py:383
msgid "created year is"
msgstr "rok utworzenia to"
#: documents/models.py:384
msgid "created month is"
msgstr "miesiąc utworzenia to"
#: documents/models.py:385
msgid "created day is"
msgstr "dzień utworzenia to"
#: documents/models.py:386
msgid "added before"
msgstr "dodany przed"
#: documents/models.py:387
msgid "added after"
msgstr "dodany po"
#: documents/models.py:388
msgid "modified before"
msgstr "zmodyfikowany przed"
#: documents/models.py:389
msgid "modified after"
msgstr "zmodyfikowany po"
#: documents/models.py:390
msgid "does not have tag"
msgstr "nie ma tagu"
#: documents/models.py:391
msgid "does not have ASN"
msgstr "nie ma numeru archiwum"
#: documents/models.py:392
msgid "title or content contains"
msgstr "tytuł lub zawartość zawiera"
#: documents/models.py:393
msgid "fulltext query"
msgstr "zapytanie pełnotekstowe"
#: documents/models.py:394
msgid "more like this"
msgstr "podobne dokumenty"
#: documents/models.py:405
msgid "rule type"
msgstr "typ reguły"
#: documents/models.py:409
msgid "value"
msgstr "wartość"
#: documents/models.py:415
msgid "filter rule"
msgstr "reguła filtrowania"
#: documents/models.py:416
msgid "filter rules"
msgstr "reguły filtrowania"
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "Nieprawidłowe wyrażenie regularne: %(error)s"
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr "Nieprawidłowy kolor."
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "Typ pliku %(type)s nie jest obsługiwany"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "Ładowanie Paperless-ng..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "Wylogowano z Paperless-ng"
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "Poprawnie wylogowano. Do zobaczenia!"
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "Zaloguj się ponownie"
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "Logowanie do Paperless-ng"
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "Proszę się zalogować."
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr "Twoja nazwa użytkownika i hasło nie są zgodne. Spróbuj ponownie."
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "Użytkownik"
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "Hasło"
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "Zaloguj się"
#: paperless/settings.py:303
msgid "English (US)"
msgstr "Angielski (USA)"
#: paperless/settings.py:304
msgid "English (GB)"
msgstr "Angielski (Wielka Brytania)"
#: paperless/settings.py:305
msgid "German"
msgstr "Niemiecki"
#: paperless/settings.py:306
msgid "Dutch"
msgstr "Holenderski"
#: paperless/settings.py:307
msgid "French"
msgstr "Francuski"
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr "Portugalski (Brazylia)"
#: paperless/settings.py:309
msgid "Portuguese"
msgstr "Portugalski"
#: paperless/settings.py:310
msgid "Italian"
msgstr "Włoski"
#: paperless/settings.py:311
msgid "Romanian"
msgstr "Rumuński"
#: paperless/settings.py:312
msgid "Russian"
msgstr "Rosyjski"
#: paperless/settings.py:313
msgid "Spanish"
msgstr "Hiszpański"
#: paperless/settings.py:314
msgid "Polish"
msgstr "Polski"
#: paperless/settings.py:315
msgid "Swedish"
msgstr "Szwedzki"
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "Administracja Paperless-ng"
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr "Uwierzytelnianie"
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr "Ustawienia zaawansowane"
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Filtry"
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless przetworzy tylko wiadomości pasujące do WSZYSTKICH filtrów podanych poniżej."
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Akcje"
#: paperless_mail/admin.py:51
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 "Akcja zastosowana do wiadomości. Ta akcja jest wykonywana tylko wtedy, gdy dokumenty zostały przetworzone z wiadomości. Poczta bez załączników pozostanie całkowicie niezmieniona."
#: paperless_mail/admin.py:58
msgid "Metadata"
msgstr "Metadane"
#: paperless_mail/admin.py:60
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 "Przypisz metadane do dokumentów zużywanych z tej reguły automatycznie. Jeśli nie przypisujesz tutaj tagów, typów lub korespondentów, Paperless będzie nadal przetwarzał wszystkie zdefiniowane przez Ciebie reguły."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Poczta Paperless"
#: paperless_mail/models.py:11
msgid "mail account"
msgstr "konto pocztowe"
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr "konta pocztowe"
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr "Brak szyfrowania"
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr "Użyj SSL"
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr "Użyj STARTTLS"
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr "Serwer IMAP"
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr "Port IMAP"
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "Zwykle jest to 143 dla połączeń niezaszyfrowanych i STARTTLS oraz 993 dla połączeń SSL."
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr "Zabezpieczenia IMAP"
#: paperless_mail/models.py:46
msgid "username"
msgstr "nazwa użytkownika"
#: paperless_mail/models.py:50
msgid "password"
msgstr "hasło"
#: paperless_mail/models.py:54
msgid "character set"
msgstr "Kodowanie"
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr "Zestaw znaków używany podczas komunikowania się z serwerem poczty, np. \"UTF-8\" lub \"US-ASCII\"."
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "reguła wiadomości"
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "reguły wiadomości"
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "Przetwarzaj tylko załączniki."
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "Przetwarzaj wszystkie pliki, łącznie z załącznikami „inline”."
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "Oznacz jako przeczytane, nie przetwarzaj przeczytanych wiadomości"
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "Oznacz wiadomość, nie przetwarzaj oznaczonych wiadomości"
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "Przenieś do określonego folderu"
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Usuń"
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "Użyj tematu jako tytułu"
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "Użyj nazwy pliku załącznika jako tytułu"
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "Nie przypisuj korespondenta"
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "Użyj adresu e-mail"
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "Użyj nazwy nadawcy (lub adresu e-mail, jeśli jest niedostępna)"
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "Użyj korespondenta wybranego poniżej"
#: paperless_mail/models.py:121
msgid "order"
msgstr "kolejność"
#: paperless_mail/models.py:128
msgid "account"
msgstr "konto"
#: paperless_mail/models.py:132
msgid "folder"
msgstr "folder"
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr "Podfoldery muszą być oddzielone kropkami."
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "filtruj po nadawcy"
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "filtruj po temacie"
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "filtruj po treści"
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "filtruj po nazwie pliku załącznika"
#: paperless_mail/models.py:150
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "Przetwarzaj tylko dokumenty, które całkowicie pasują do tej nazwy pliku, jeśli jest podana. Wzorce dopasowania jak *.pdf lub *faktura* są dozwolone. Wielkość liter nie jest rozróżniana."
#: paperless_mail/models.py:156
msgid "maximum age"
msgstr "nie starsze niż"
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "dni."
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "typ załącznika"
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "Załączniki typu \"inline\" zawierają osadzone obrazy, więc najlepiej połączyć tę opcję z filtrem nazwy pliku."
#: paperless_mail/models.py:169
msgid "action"
msgstr "akcja"
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "parametr akcji"
#: paperless_mail/models.py:177
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 "Dodatkowy parametr dla akcji wybranej powyżej, tj. docelowy folder akcji \"Przenieś do określonego folderu\". Podfoldery muszą być oddzielone kropkami."
#: paperless_mail/models.py:184
msgid "assign title from"
msgstr "przypisz tytuł"
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "przypisz ten tag"
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "przypisz ten typ dokumentu"
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "przypisz korespondenta z"
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "przypisz tego korespondenta"

View File

@@ -0,0 +1,699 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-05-16 10:09\n"
"Last-Translator: \n"
"Language-Team: Portuguese, Brazilian\n"
"Language: pt_BR\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-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: pt-BR\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Documentos"
#: documents/models.py:32
msgid "Any word"
msgstr "Qualquer palavra"
#: documents/models.py:33
msgid "All words"
msgstr "Todas as palavras"
#: documents/models.py:34
msgid "Exact match"
msgstr "Detecção exata"
#: documents/models.py:35
msgid "Regular expression"
msgstr "Expressão regular"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Palavra difusa (fuzzy)"
#: documents/models.py:37
msgid "Automatic"
msgstr "Automático"
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "nome"
#: documents/models.py:45
msgid "match"
msgstr "detecção"
#: documents/models.py:49
msgid "matching algorithm"
msgstr "algoritmo de detecção"
#: documents/models.py:55
msgid "is insensitive"
msgstr "diferencia maiúsculas de minúsculas"
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "correspondente"
#: documents/models.py:75
msgid "correspondents"
msgstr "correspondentes"
#: documents/models.py:81
msgid "color"
msgstr "cor"
#: documents/models.py:87
msgid "is inbox tag"
msgstr "é etiqueta caixa de entrada"
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Marca essa etiqueta como caixa de entrada: Todos os novos documentos consumidos terão as etiquetas de caixa de entrada."
#: documents/models.py:94
msgid "tag"
msgstr "etiqueta"
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "etiquetas"
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "tipo de documento"
#: documents/models.py:102
msgid "document types"
msgstr "tipos de documento"
#: documents/models.py:110
msgid "Unencrypted"
msgstr "Não encriptado"
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "Encriptado com GNU Privacy Guard"
#: documents/models.py:124
msgid "title"
msgstr "título"
#: documents/models.py:137
msgid "content"
msgstr "conteúdo"
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "O conteúdo de texto bruto do documento. Esse campo é usado principalmente para busca."
#: documents/models.py:144
msgid "mime type"
msgstr "tipo mime"
#: documents/models.py:155
msgid "checksum"
msgstr "some de verificação"
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "A soma de verificação original do documento."
#: documents/models.py:163
msgid "archive checksum"
msgstr "Soma de verificação de arquivamento."
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "A soma de verificação do documento arquivado."
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "criado"
#: documents/models.py:176
msgid "modified"
msgstr "modificado"
#: documents/models.py:180
msgid "storage type"
msgstr "tipo de armazenamento"
#: documents/models.py:188
msgid "added"
msgstr "adicionado"
#: documents/models.py:192
msgid "filename"
msgstr "nome do arquivo"
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "Nome do arquivo atual armazenado"
#: documents/models.py:202
msgid "archive filename"
msgstr "nome do arquivo para arquivamento"
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr "Nome do arquivo para arquivamento armazenado"
#: documents/models.py:212
msgid "archive serial number"
msgstr "número de sério de arquivamento"
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr "A posição deste documento no seu arquivamento físico."
#: documents/models.py:223
msgid "document"
msgstr "documento"
#: documents/models.py:224
msgid "documents"
msgstr "documentos"
#: documents/models.py:311
msgid "debug"
msgstr "debug"
#: documents/models.py:312
msgid "information"
msgstr "informação"
#: documents/models.py:313
msgid "warning"
msgstr "aviso"
#: documents/models.py:314
msgid "error"
msgstr "erro"
#: documents/models.py:315
msgid "critical"
msgstr "crítico"
#: documents/models.py:319
msgid "group"
msgstr "grupo"
#: documents/models.py:322
msgid "message"
msgstr "mensagem"
#: documents/models.py:325
msgid "level"
msgstr "nível"
#: documents/models.py:332
msgid "log"
msgstr "log"
#: documents/models.py:333
msgid "logs"
msgstr "logs"
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "visualização"
#: documents/models.py:345
msgid "saved views"
msgstr "visualizações"
#: documents/models.py:348
msgid "user"
msgstr "usuário"
#: documents/models.py:354
msgid "show on dashboard"
msgstr "exibir no painel de controle"
#: documents/models.py:357
msgid "show in sidebar"
msgstr "exibir no painel lateral"
#: documents/models.py:361
msgid "sort field"
msgstr "ordenar campo"
#: documents/models.py:367
msgid "sort reverse"
msgstr "odernar reverso"
#: documents/models.py:373
msgid "title contains"
msgstr "título contém"
#: documents/models.py:374
msgid "content contains"
msgstr "conteúdo contém"
#: documents/models.py:375
msgid "ASN is"
msgstr "NSA é"
#: documents/models.py:376
msgid "correspondent is"
msgstr "correspondente é"
#: documents/models.py:377
msgid "document type is"
msgstr "tipo de documento é"
#: documents/models.py:378
msgid "is in inbox"
msgstr "é caixa de entrada"
#: documents/models.py:379
msgid "has tag"
msgstr "contém etiqueta"
#: documents/models.py:380
msgid "has any tag"
msgstr "contém qualquer etiqueta"
#: documents/models.py:381
msgid "created before"
msgstr "criado antes de"
#: documents/models.py:382
msgid "created after"
msgstr "criado depois de"
#: documents/models.py:383
msgid "created year is"
msgstr "ano de criação é"
#: documents/models.py:384
msgid "created month is"
msgstr "mês de criação é"
#: documents/models.py:385
msgid "created day is"
msgstr "dia de criação é"
#: documents/models.py:386
msgid "added before"
msgstr "adicionado antes de"
#: documents/models.py:387
msgid "added after"
msgstr "adicionado depois de"
#: documents/models.py:388
msgid "modified before"
msgstr "modificado antes de"
#: documents/models.py:389
msgid "modified after"
msgstr "modificado depois de"
#: documents/models.py:390
msgid "does not have tag"
msgstr "não tem etiqueta"
#: documents/models.py:391
msgid "does not have ASN"
msgstr ""
#: documents/models.py:392
msgid "title or content contains"
msgstr "título ou conteúdo contém"
#: documents/models.py:393
msgid "fulltext query"
msgstr ""
#: documents/models.py:394
msgid "more like this"
msgstr ""
#: documents/models.py:405
msgid "rule type"
msgstr "tipo de regra"
#: documents/models.py:409
msgid "value"
msgstr "valor"
#: documents/models.py:415
msgid "filter rule"
msgstr "regra de filtragem"
#: documents/models.py:416
msgid "filter rules"
msgstr "regras de filtragem"
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "Expressão regular inválida: %(error)s"
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr "Cor inválida."
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "Tipo de arquivo %(type)s não suportado"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "Paperless-ng está carregando..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "Paperless-ng saiu"
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "Sua sessão foi encerrada com sucesso. Até mais!"
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "Entre novamente"
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "Entrar no Paperless-ng"
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "Por favor, entre na sua conta"
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr "Seu usuário e senha estão incorretos. Por favor, tente novamente."
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "Usuário"
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "Senha"
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "Entrar"
#: paperless/settings.py:303
msgid "English (US)"
msgstr "Inglês (EUA)"
#: paperless/settings.py:304
msgid "English (GB)"
msgstr "Inglês (GB)"
#: paperless/settings.py:305
msgid "German"
msgstr "Alemão"
#: paperless/settings.py:306
msgid "Dutch"
msgstr "Holandês"
#: paperless/settings.py:307
msgid "French"
msgstr "Francês"
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr "Português (Brasil)"
#: paperless/settings.py:309
msgid "Portuguese"
msgstr ""
#: paperless/settings.py:310
msgid "Italian"
msgstr "Italiano"
#: paperless/settings.py:311
msgid "Romanian"
msgstr "Romeno"
#: paperless/settings.py:312
msgid "Russian"
msgstr ""
#: paperless/settings.py:313
msgid "Spanish"
msgstr ""
#: paperless/settings.py:314
msgid "Polish"
msgstr ""
#: paperless/settings.py:315
msgid "Swedish"
msgstr ""
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "Administração do Paperless-ng"
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr ""
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr ""
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Filtro"
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless processará somente e-mails que se encaixam em TODOS os filtros abaixo."
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Ações"
#: paperless_mail/admin.py:51
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 "A ação se aplica ao e-mail. Essa ação só é executada quando documentos foram consumidos do e-mail. E-mails sem anexos permanecerão intactos."
#: paperless_mail/admin.py:58
msgid "Metadata"
msgstr "Metadados"
#: paperless_mail/admin.py:60
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 "Atribua metadados aos documentos consumidos por esta regra automaticamente. Se você não atribuir etiquetas, tipos ou correspondentes aqui, paperless ainda sim processará todas as regras de detecção que você definiu."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Paperless mail"
#: paperless_mail/models.py:11
msgid "mail account"
msgstr "conta de e-mail"
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr "contas de e-mail"
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr "Sem encriptação"
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr "Usar SSL"
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr "Usar STARTTLS"
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr "Servidor IMAP"
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr "Porta IMAP"
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "É geralmente 143 para não encriptado e conexões STARTTLS, e 993 para conexões SSL."
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr "segurança IMAP"
#: paperless_mail/models.py:46
msgid "username"
msgstr "usuário"
#: paperless_mail/models.py:50
msgid "password"
msgstr "senha"
#: paperless_mail/models.py:54
msgid "character set"
msgstr ""
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr ""
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "regra de e-mail"
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "regras de e-mail"
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "Processar somente anexos."
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "Processar todos os arquivos, incluindo anexos 'inline'."
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "Marcar como lido, não processar e-mails lidos"
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "Sinalizar o e-mail, não processar e-mails sinalizados"
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "Mover para pasta especificada"
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Excluir"
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "Usar assunto como título"
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "Usar nome do arquivo anexo como título"
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "Não atribuir um correspondente"
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "Usar endereço de e-mail"
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "Usar nome (ou endereço de e-mail se não disponível)"
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "Usar correspondente selecionado abaixo"
#: paperless_mail/models.py:121
msgid "order"
msgstr "ordem"
#: paperless_mail/models.py:128
msgid "account"
msgstr "conta"
#: paperless_mail/models.py:132
msgid "folder"
msgstr "pasta"
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr ""
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "filtrar de"
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "filtrar assunto"
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "filtrar corpo"
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "filtrar nome do arquivo anexo"
#: paperless_mail/models.py:150
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "Consumir somente documentos que correspondem a este nome de arquivo se especificado.\n"
"Curingas como *.pdf ou *invoice* são permitidos. Sem diferenciação de maiúsculas e minúsculas."
#: paperless_mail/models.py:156
msgid "maximum age"
msgstr "idade máxima"
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "Especificada em dias."
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "tipo de anexo"
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "Anexos inline incluem imagens inseridas, por isso é melhor combinar essa opção com um filtro de nome de arquivo."
#: paperless_mail/models.py:169
msgid "action"
msgstr "ação"
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "parâmetro da ação"
#: paperless_mail/models.py:177
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:184
msgid "assign title from"
msgstr "atribuir título de"
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "atribuir esta etiqueta"
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "atribuir este tipo de documento"
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "atribuir correspondente de"
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "atribuir este correspondente"

View File

@@ -0,0 +1,698 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-05-16 20:45\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\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-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: pt-PT\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Documentos"
#: documents/models.py:32
msgid "Any word"
msgstr "Qualquer palavra"
#: documents/models.py:33
msgid "All words"
msgstr "Todas as palavras"
#: documents/models.py:34
msgid "Exact match"
msgstr "Detecção exata"
#: documents/models.py:35
msgid "Regular expression"
msgstr "Expressão regular"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Palavra difusa (fuzzy)"
#: documents/models.py:37
msgid "Automatic"
msgstr "Automático"
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "nome"
#: documents/models.py:45
msgid "match"
msgstr "correspondência"
#: documents/models.py:49
msgid "matching algorithm"
msgstr "algoritmo correspondente"
#: documents/models.py:55
msgid "is insensitive"
msgstr "é insensível"
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "correspondente"
#: documents/models.py:75
msgid "correspondents"
msgstr "correspondentes"
#: documents/models.py:81
msgid "color"
msgstr "cor"
#: documents/models.py:87
msgid "is inbox tag"
msgstr "é etiqueta de novo"
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Marca esta etiqueta como uma etiqueta de entrada. Todos os documentos recentemente consumidos serão etiquetados com a etiqueta de entrada."
#: documents/models.py:94
msgid "tag"
msgstr "etiqueta"
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "etiquetas"
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "tipo de documento"
#: documents/models.py:102
msgid "document types"
msgstr "tipos de documento"
#: documents/models.py:110
msgid "Unencrypted"
msgstr "Não encriptado"
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "Encriptado com GNU Privacy Guard"
#: documents/models.py:124
msgid "title"
msgstr "título"
#: documents/models.py:137
msgid "content"
msgstr "conteúdo"
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "Os dados de texto, em cru, do documento. Este campo é utilizado principalmente para pesquisar."
#: documents/models.py:144
msgid "mime type"
msgstr "tipo mime"
#: documents/models.py:155
msgid "checksum"
msgstr "soma de verificação"
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "A soma de verificação do documento original."
#: documents/models.py:163
msgid "archive checksum"
msgstr "arquivar soma de verificação"
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "A soma de verificação do documento arquivado."
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "criado"
#: documents/models.py:176
msgid "modified"
msgstr "modificado"
#: documents/models.py:180
msgid "storage type"
msgstr "tipo de armazenamento"
#: documents/models.py:188
msgid "added"
msgstr "adicionado"
#: documents/models.py:192
msgid "filename"
msgstr "nome de ficheiro"
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "Nome do arquivo atual no armazenamento"
#: documents/models.py:202
msgid "archive filename"
msgstr "nome do ficheiro de arquivo"
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr "Nome do arquivo atual em no armazenamento"
#: documents/models.py:212
msgid "archive serial number"
msgstr "numero de série de arquivo"
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr "A posição do documento no seu arquivo físico de documentos."
#: documents/models.py:223
msgid "document"
msgstr "documento"
#: documents/models.py:224
msgid "documents"
msgstr "documentos"
#: documents/models.py:311
msgid "debug"
msgstr "depurar"
#: documents/models.py:312
msgid "information"
msgstr "informação"
#: documents/models.py:313
msgid "warning"
msgstr "aviso"
#: documents/models.py:314
msgid "error"
msgstr "erro"
#: documents/models.py:315
msgid "critical"
msgstr "crítico"
#: documents/models.py:319
msgid "group"
msgstr "grupo"
#: documents/models.py:322
msgid "message"
msgstr "mensagem"
#: documents/models.py:325
msgid "level"
msgstr "nível"
#: documents/models.py:332
msgid "log"
msgstr "registo"
#: documents/models.py:333
msgid "logs"
msgstr "registos"
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "vista guardada"
#: documents/models.py:345
msgid "saved views"
msgstr "vistas guardadas"
#: documents/models.py:348
msgid "user"
msgstr "utilizador"
#: documents/models.py:354
msgid "show on dashboard"
msgstr "exibir no painel de controlo"
#: documents/models.py:357
msgid "show in sidebar"
msgstr "mostrar na navegação lateral"
#: documents/models.py:361
msgid "sort field"
msgstr "ordenar campo"
#: documents/models.py:367
msgid "sort reverse"
msgstr "ordenar inversamente"
#: documents/models.py:373
msgid "title contains"
msgstr "o título contém"
#: documents/models.py:374
msgid "content contains"
msgstr "o conteúdo contém"
#: documents/models.py:375
msgid "ASN is"
msgstr "O NSA é"
#: documents/models.py:376
msgid "correspondent is"
msgstr "o correspondente é"
#: documents/models.py:377
msgid "document type is"
msgstr "o tipo de documento é"
#: documents/models.py:378
msgid "is in inbox"
msgstr "está na entrada"
#: documents/models.py:379
msgid "has tag"
msgstr "tem etiqueta"
#: documents/models.py:380
msgid "has any tag"
msgstr "tem qualquer etiqueta"
#: documents/models.py:381
msgid "created before"
msgstr "criado antes"
#: documents/models.py:382
msgid "created after"
msgstr "criado depois"
#: documents/models.py:383
msgid "created year is"
msgstr "ano criada é"
#: documents/models.py:384
msgid "created month is"
msgstr "mês criado é"
#: documents/models.py:385
msgid "created day is"
msgstr "dia criado é"
#: documents/models.py:386
msgid "added before"
msgstr "adicionada antes"
#: documents/models.py:387
msgid "added after"
msgstr "adicionado depois de"
#: documents/models.py:388
msgid "modified before"
msgstr "modificado antes de"
#: documents/models.py:389
msgid "modified after"
msgstr "modificado depois de"
#: documents/models.py:390
msgid "does not have tag"
msgstr "não tem etiqueta"
#: documents/models.py:391
msgid "does not have ASN"
msgstr "não possui um NSA"
#: documents/models.py:392
msgid "title or content contains"
msgstr "título ou conteúdo contém"
#: documents/models.py:393
msgid "fulltext query"
msgstr "consulta de texto completo"
#: documents/models.py:394
msgid "more like this"
msgstr "mais como este"
#: documents/models.py:405
msgid "rule type"
msgstr "tipo de regra"
#: documents/models.py:409
msgid "value"
msgstr "valor"
#: documents/models.py:415
msgid "filter rule"
msgstr "regra de filtragem"
#: documents/models.py:416
msgid "filter rules"
msgstr "regras de filtragem"
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "Expressão regular inválida: %(error)s"
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr "Cor invalida."
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "Tipo de arquivo %(type)s não suportado"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "O paperless-ng está a carregar..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "Paperless-ng com sessão terminada"
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "Terminou a sessão com sucesso. Adeus!"
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "Iniciar sessão novamente"
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "Inicio de sessão Paperless-ng"
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "Por favor inicie sessão."
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr "O utilizador e a senha não correspondem. Tente novamente."
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "Nome de utilizador"
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "Palavra-passe"
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "Iniciar sessão"
#: paperless/settings.py:303
msgid "English (US)"
msgstr "Inglês (EUA)"
#: paperless/settings.py:304
msgid "English (GB)"
msgstr "English (GB)"
#: paperless/settings.py:305
msgid "German"
msgstr "Deutsch"
#: paperless/settings.py:306
msgid "Dutch"
msgstr "Nederlandse"
#: paperless/settings.py:307
msgid "French"
msgstr "Français"
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr "Português (Brasil)"
#: paperless/settings.py:309
msgid "Portuguese"
msgstr "Português"
#: paperless/settings.py:310
msgid "Italian"
msgstr "Italiano"
#: paperless/settings.py:311
msgid "Romanian"
msgstr "Romeno"
#: paperless/settings.py:312
msgid "Russian"
msgstr "Russo"
#: paperless/settings.py:313
msgid "Spanish"
msgstr "Espanhol"
#: paperless/settings.py:314
msgid "Polish"
msgstr "Polaco"
#: paperless/settings.py:315
msgid "Swedish"
msgstr "Sueco"
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "Administração do Paperless-ng"
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr "Autenticação"
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr "Definições avançadas"
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Filtro"
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "O Paperless apenas irá processar emails que coincidem com TODOS os filtros dados abaixo."
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Ações"
#: paperless_mail/admin.py:51
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 "A ação aplicada a correio. Esta ação apenas será efetuada com documentos que tenham sido consumidos através do correio. E-mails sem anexos permanecerão intactos."
#: paperless_mail/admin.py:58
msgid "Metadata"
msgstr "Metadados"
#: paperless_mail/admin.py:60
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 "Atribuir meta-dados aos documentos consumidos automaticamente através desta regra. Se você não atribuir etiquetas, tipos ou correspondentes aqui, o paperless ainda assim processará todas as regras correspondentes que tenha definido."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Correio Paperless"
#: paperless_mail/models.py:11
msgid "mail account"
msgstr "conta de email"
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr "contas de email"
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr "Sem encriptação"
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr "Utilizar SSL"
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr "Utilizar STARTTLS"
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr "Servidor IMAP"
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr "Porto IMAP"
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "Por norma é o 143 sem encriptação e conexões STARTTLS, e o 993 para conexões com SSL."
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr "Segurança IMAP"
#: paperless_mail/models.py:46
msgid "username"
msgstr "nome de utilizador"
#: paperless_mail/models.py:50
msgid "password"
msgstr "palavra-passe"
#: paperless_mail/models.py:54
msgid "character set"
msgstr "conjunto de caracteres"
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr "O conjunto de caracteres a utilizar ao comunicar com um servidor de email, tal como 'UTF-8' ou 'US-ASCII'."
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "regra de correio"
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "regras de correio"
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "Processar anexos apenas."
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "Processar todos os ficheiros, incluindo ficheiros 'embutidos (inline)'."
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "Marcar como lido, não processar emails lidos"
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "Marcar o email, não processar emails marcados"
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "Mover para uma diretoria específica"
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Excluir"
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "Utilizar o assunto como título"
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "Utilizar o nome do anexo como título"
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "Não atribuir um correspondente"
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "Utilizar o endereço de email"
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "Utilizar nome (ou endereço de email se não disponível)"
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "Utilizar o correspondente selecionado abaixo"
#: paperless_mail/models.py:121
msgid "order"
msgstr "ordem"
#: paperless_mail/models.py:128
msgid "account"
msgstr "conta"
#: paperless_mail/models.py:132
msgid "folder"
msgstr "directoria"
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr "Sub-pastas devem ser separadas por pontos."
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "filtrar de"
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "filtrar assunto"
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "filtrar corpo"
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "filtrar nome do arquivo anexo"
#: paperless_mail/models.py:150
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "Consumir apenas documentos que correspondam inteiramente ao nome de arquivo se especificado. Genéricos como *.pdf ou *fatura* são permitidos. Não é sensível a letras maiúsculas/minúsculas."
#: paperless_mail/models.py:156
msgid "maximum age"
msgstr "idade máxima"
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "Especificado em dias."
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "tipo de anexo"
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "Anexos embutidos incluem imagens incorporadas, por isso é melhor combinar esta opção com um filtro de nome do arquivo."
#: paperless_mail/models.py:169
msgid "action"
msgstr "ação"
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "parâmetro de ação"
#: paperless_mail/models.py:177
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 "Parâmetros adicionais para a ação selecionada acima, isto é, a pasta alvo da ação mover para pasta. Sub-pastas devem ser separadas por pontos."
#: paperless_mail/models.py:184
msgid "assign title from"
msgstr "atribuir titulo de"
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "atribuir esta etiqueta"
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "atribuir este tipo de documento"
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "atribuir correspondente de"
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "atribuir este correspondente"

View File

@@ -0,0 +1,698 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-08-16 09:06\n"
"Last-Translator: \n"
"Language-Team: Romanian\n"
"Language: ro_RO\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n"
"X-Crowdin-Project: paperless-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: ro\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Documente"
#: documents/models.py:32
msgid "Any word"
msgstr "Orice cuvânt"
#: documents/models.py:33
msgid "All words"
msgstr "Toate cuvintele"
#: documents/models.py:34
msgid "Exact match"
msgstr "Potrivire exactă"
#: documents/models.py:35
msgid "Regular expression"
msgstr "Expresie regulată"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Mod neatent"
#: documents/models.py:37
msgid "Automatic"
msgstr "Automat"
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "nume"
#: documents/models.py:45
msgid "match"
msgstr "potrivire"
#: documents/models.py:49
msgid "matching algorithm"
msgstr "algoritm de potrivire"
#: documents/models.py:55
msgid "is insensitive"
msgstr "nu ține cont de majuscule"
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "corespondent"
#: documents/models.py:75
msgid "correspondents"
msgstr "corespondenți"
#: documents/models.py:81
msgid "color"
msgstr "culoare"
#: documents/models.py:87
msgid "is inbox tag"
msgstr "este etichetă inbox"
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Marchează aceasta eticheta ca etichetă inbox: Toate documentele nou consumate primesc aceasta eticheta."
#: documents/models.py:94
msgid "tag"
msgstr "etichetă"
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "etichete"
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "tip de document"
#: documents/models.py:102
msgid "document types"
msgstr "tipuri de document"
#: documents/models.py:110
msgid "Unencrypted"
msgstr "Necriptat"
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "Criptat cu GNU Privacy Guard"
#: documents/models.py:124
msgid "title"
msgstr "titlu"
#: documents/models.py:137
msgid "content"
msgstr "conținut"
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "Textul brut al documentului. Acest camp este folosit in principal pentru căutare."
#: documents/models.py:144
msgid "mime type"
msgstr "tip MIME"
#: documents/models.py:155
msgid "checksum"
msgstr "sumă de control"
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "Suma de control a documentului original."
#: documents/models.py:163
msgid "archive checksum"
msgstr "suma de control a arhivei"
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "Suma de control a documentului arhivat."
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "creat"
#: documents/models.py:176
msgid "modified"
msgstr "modificat"
#: documents/models.py:180
msgid "storage type"
msgstr "tip de stocare"
#: documents/models.py:188
msgid "added"
msgstr "adăugat"
#: documents/models.py:192
msgid "filename"
msgstr "nume fișier"
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "Numele curent al fișierului stocat"
#: documents/models.py:202
msgid "archive filename"
msgstr "nume fișier arhiva"
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr "Numele curent al arhivei stocate"
#: documents/models.py:212
msgid "archive serial number"
msgstr "număr serial in arhiva"
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr "Poziția acestui document in arhiva fizica."
#: documents/models.py:223
msgid "document"
msgstr "document"
#: documents/models.py:224
msgid "documents"
msgstr "documente"
#: documents/models.py:311
msgid "debug"
msgstr "depanare"
#: documents/models.py:312
msgid "information"
msgstr "informații"
#: documents/models.py:313
msgid "warning"
msgstr "avertizare"
#: documents/models.py:314
msgid "error"
msgstr "eroare"
#: documents/models.py:315
msgid "critical"
msgstr "critic"
#: documents/models.py:319
msgid "group"
msgstr "grup"
#: documents/models.py:322
msgid "message"
msgstr "mesaj"
#: documents/models.py:325
msgid "level"
msgstr "nivel"
#: documents/models.py:332
msgid "log"
msgstr "jurnal"
#: documents/models.py:333
msgid "logs"
msgstr "jurnale"
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "vizualizare"
#: documents/models.py:345
msgid "saved views"
msgstr "vizualizări"
#: documents/models.py:348
msgid "user"
msgstr "utilizator"
#: documents/models.py:354
msgid "show on dashboard"
msgstr "afișează pe tabloul de bord"
#: documents/models.py:357
msgid "show in sidebar"
msgstr "afișează in bara laterala"
#: documents/models.py:361
msgid "sort field"
msgstr "sortează camp"
#: documents/models.py:367
msgid "sort reverse"
msgstr "sortează invers"
#: documents/models.py:373
msgid "title contains"
msgstr "titlul conține"
#: documents/models.py:374
msgid "content contains"
msgstr "conținutul conține"
#: documents/models.py:375
msgid "ASN is"
msgstr "Avizul prealabil de expediție este"
#: documents/models.py:376
msgid "correspondent is"
msgstr "corespondentul este"
#: documents/models.py:377
msgid "document type is"
msgstr "tipul documentului este"
#: documents/models.py:378
msgid "is in inbox"
msgstr "este în inbox"
#: documents/models.py:379
msgid "has tag"
msgstr "are eticheta"
#: documents/models.py:380
msgid "has any tag"
msgstr "are orice eticheta"
#: documents/models.py:381
msgid "created before"
msgstr "creat înainte de"
#: documents/models.py:382
msgid "created after"
msgstr "creat după"
#: documents/models.py:383
msgid "created year is"
msgstr "anul creării este"
#: documents/models.py:384
msgid "created month is"
msgstr "luna creării este"
#: documents/models.py:385
msgid "created day is"
msgstr "ziua creării este"
#: documents/models.py:386
msgid "added before"
msgstr "adăugat înainte de"
#: documents/models.py:387
msgid "added after"
msgstr "adăugat după"
#: documents/models.py:388
msgid "modified before"
msgstr "modificat înainte de"
#: documents/models.py:389
msgid "modified after"
msgstr "modificat după"
#: documents/models.py:390
msgid "does not have tag"
msgstr "nu are etichetă"
#: documents/models.py:391
msgid "does not have ASN"
msgstr "nu are aviz prealabil de expediție"
#: documents/models.py:392
msgid "title or content contains"
msgstr "titlul sau conținutul conține"
#: documents/models.py:393
msgid "fulltext query"
msgstr "query fulltext"
#: documents/models.py:394
msgid "more like this"
msgstr "mai multe ca aceasta"
#: documents/models.py:405
msgid "rule type"
msgstr "tip de regula"
#: documents/models.py:409
msgid "value"
msgstr "valoare"
#: documents/models.py:415
msgid "filter rule"
msgstr "regulă de filtrare"
#: documents/models.py:416
msgid "filter rules"
msgstr "reguli de filtrare"
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "Expresie regulată invalida: %(error)s"
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr "Culoare invalidă."
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "Tip de fișier %(type)s nesuportat"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "Paperless-ng se încarca..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "Paperless-ng s-a deconectat"
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "Ați fost deconectat cu succes. La revedere!"
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "Conectați-vă din nou"
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "Conectare Paperless-ng"
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "Vă rugăm conectați-vă."
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr "Numele si parola nu sunt corecte. Vă rugăm incercați din nou."
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "Nume"
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "Parolă"
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "Conectare"
#: paperless/settings.py:303
msgid "English (US)"
msgstr "Engleză (Americană)"
#: paperless/settings.py:304
msgid "English (GB)"
msgstr "Engleză (Britanică)"
#: paperless/settings.py:305
msgid "German"
msgstr "Germană"
#: paperless/settings.py:306
msgid "Dutch"
msgstr "Olandeză"
#: paperless/settings.py:307
msgid "French"
msgstr "Franceză"
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr "Portugheză (Brazilia)"
#: paperless/settings.py:309
msgid "Portuguese"
msgstr "Portugheză"
#: paperless/settings.py:310
msgid "Italian"
msgstr "Italiană"
#: paperless/settings.py:311
msgid "Romanian"
msgstr "Română"
#: paperless/settings.py:312
msgid "Russian"
msgstr "Rusă"
#: paperless/settings.py:313
msgid "Spanish"
msgstr "Spaniolă"
#: paperless/settings.py:314
msgid "Polish"
msgstr "Poloneză"
#: paperless/settings.py:315
msgid "Swedish"
msgstr "Suedeză"
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "Administrare Paperless-ng"
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr "Autentificare"
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr "Setări avansate"
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Filtru"
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless va procesa doar mail-urile care corespund TUTUROR filtrelor date mai jos."
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Acțiuni"
#: paperless_mail/admin.py:51
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 "Acțiunea aplicată tuturor email-urilor. Aceasta este realizată doar când sunt consumate documente din email. Cele fara atașamente nu vor fi procesate."
#: paperless_mail/admin.py:58
msgid "Metadata"
msgstr "Metadate"
#: paperless_mail/admin.py:60
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 "Atribuie metadate documentelor consumate prin aceasta regula în mod automat. Chiar dacă nu sunt atribuite etichete, tipuri sau corespondenți, Paperless va procesa toate regulile definite care se potrivesc."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Email Paperless"
#: paperless_mail/models.py:11
msgid "mail account"
msgstr "cont de email"
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr "conturi de email"
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr "Fără criptare"
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr "Folosește SSL"
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr "Folosește STARTTLS"
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr "server IMAP"
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr "port IMAP"
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "De obicei este 143 pentru conexiuni necriptate și STARTTLS, sau 993 pentru conexiuni SSL."
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr "securitate IMAP"
#: paperless_mail/models.py:46
msgid "username"
msgstr "nume"
#: paperless_mail/models.py:50
msgid "password"
msgstr "parolă"
#: paperless_mail/models.py:54
msgid "character set"
msgstr "Set de caractere"
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr "Setul de caractere folosit la comunicarea cu serverul de e-mail, cum ar fi \"UTF-8\" sau \"US-ASCII\"."
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "regulă email"
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "reguli email"
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "Procesează doar atașamentele."
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "Procesează toate fișierele, inclusiv atașamentele „inline”."
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "Marchează ca citit, nu procesa email-uri citite"
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "Marchează, nu procesa email-uri marcate"
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "Mută în directorul specificat"
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Șterge"
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "Utilizează subiectul ca titlu"
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "Utilizează numele fișierului atașat ca titlu"
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "Nu atribui un corespondent"
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "Folosește adresa de email"
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "Folosește numele (dacă nu exista, folosește adresa de email)"
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "Folosește corespondentul selectat mai jos"
#: paperless_mail/models.py:121
msgid "order"
msgstr "ordonează"
#: paperless_mail/models.py:128
msgid "account"
msgstr "cont"
#: paperless_mail/models.py:132
msgid "folder"
msgstr "director"
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr "Subdosarele trebuie separate prin puncte."
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "filtrează de la"
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "filtrează subiect"
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "filtrează corpul email-ului"
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "filtrează numele fișierului atașat"
#: paperless_mail/models.py:150
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "Consumă doar documentele care se potrivesc în întregime cu acest nume de fișier, dacă este specificat. Simbolul * ține locul oricărui șir de caractere. Majusculele nu contează."
#: paperless_mail/models.py:156
msgid "maximum age"
msgstr "vârsta maximă"
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "Specificată in zile."
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "tip atașament"
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "Atașamentele \"inline\" includ și imaginile încorporate, deci această opțiune funcționează cel mai bine combinată cu un filtru pentru numele fișierului."
#: paperless_mail/models.py:169
msgid "action"
msgstr "acţiune"
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "parametru acțiune"
#: paperless_mail/models.py:177
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 "Parametru adițional pentru acțiunea definită mai sus (ex. directorul în care să se realizeze o mutare). Subdosarele trebuie separate prin puncte."
#: paperless_mail/models.py:184
msgid "assign title from"
msgstr "atribuie titlu din"
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "atribuie această etichetă"
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "atribuie acest tip"
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "atribuie corespondent din"
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "atribuie acest corespondent"

View File

@@ -0,0 +1,698 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-05-16 10:09\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\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-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: ru\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\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:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "имя"
#: documents/models.py:45
msgid "match"
msgstr "соответствие"
#: documents/models.py:49
msgid "matching algorithm"
msgstr "алгоритм сопоставления"
#: documents/models.py:55
msgid "is insensitive"
msgstr "без учёта регистра"
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "корреспондент"
#: documents/models.py:75
msgid "correspondents"
msgstr "корреспонденты"
#: documents/models.py:81
msgid "color"
msgstr "цвет"
#: documents/models.py:87
msgid "is inbox tag"
msgstr "это входящий тег"
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Отметить этот тег как «Входящий»: все вновь добавленные документы будут помечены тегами «Входящие»."
#: documents/models.py:94
msgid "tag"
msgstr "тег"
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "теги"
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "тип документа"
#: documents/models.py:102
msgid "document types"
msgstr "типы документов"
#: documents/models.py:110
msgid "Unencrypted"
msgstr "не зашифровано"
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "Зашифровано с помощью GNU Privacy Guard"
#: documents/models.py:124
msgid "title"
msgstr "заголовок"
#: documents/models.py:137
msgid "content"
msgstr "содержимое"
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "Это поле используется в основном для поиска."
#: documents/models.py:144
msgid "mime type"
msgstr "тип Mime"
#: documents/models.py:155
msgid "checksum"
msgstr "контрольная сумма"
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "Контрольная сумма оригинального документа."
#: documents/models.py:163
msgid "archive checksum"
msgstr "контрольная сумма архива"
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "Контрольная сумма архивного документа."
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "создано"
#: documents/models.py:176
msgid "modified"
msgstr "изменено"
#: documents/models.py:180
msgid "storage type"
msgstr "тип хранилища"
#: documents/models.py:188
msgid "added"
msgstr "добавлено"
#: documents/models.py:192
msgid "filename"
msgstr "имя файла"
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "Текущее имя файла в хранилище"
#: documents/models.py:202
msgid "archive filename"
msgstr "имя файла архива"
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr "Текущее имя файла архива в хранилище"
#: documents/models.py:212
msgid "archive serial number"
msgstr "архивный номер (АН)"
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr "Позиция этого документа в вашем физическом архиве документов."
#: documents/models.py:223
msgid "document"
msgstr "документ"
#: documents/models.py:224
msgid "documents"
msgstr "документы"
#: documents/models.py:311
msgid "debug"
msgstr "отладка"
#: documents/models.py:312
msgid "information"
msgstr "информация"
#: documents/models.py:313
msgid "warning"
msgstr "предупреждение"
#: documents/models.py:314
msgid "error"
msgstr "ошибка"
#: documents/models.py:315
msgid "critical"
msgstr "критическая"
#: documents/models.py:319
msgid "group"
msgstr "группа"
#: documents/models.py:322
msgid "message"
msgstr "сообщение"
#: documents/models.py:325
msgid "level"
msgstr "уровень"
#: documents/models.py:332
msgid "log"
msgstr "журнал"
#: documents/models.py:333
msgid "logs"
msgstr "логи"
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "сохранённое представление"
#: documents/models.py:345
msgid "saved views"
msgstr "сохраненные представления"
#: documents/models.py:348
msgid "user"
msgstr "пользователь"
#: documents/models.py:354
msgid "show on dashboard"
msgstr "показать на панели"
#: documents/models.py:357
msgid "show in sidebar"
msgstr "показать в боковой панели"
#: documents/models.py:361
msgid "sort field"
msgstr "Поле сортировки"
#: documents/models.py:367
msgid "sort reverse"
msgstr "обратная сортировка"
#: documents/models.py:373
msgid "title contains"
msgstr "заголовок содержит"
#: documents/models.py:374
msgid "content contains"
msgstr "содержимое содержит"
#: documents/models.py:375
msgid "ASN is"
msgstr "АН"
#: documents/models.py:376
msgid "correspondent is"
msgstr "корреспондент"
#: documents/models.py:377
msgid "document type is"
msgstr "тип документа"
#: documents/models.py:378
msgid "is in inbox"
msgstr "во входящих"
#: documents/models.py:379
msgid "has tag"
msgstr "есть тег"
#: documents/models.py:380
msgid "has any tag"
msgstr "есть любой тег"
#: documents/models.py:381
msgid "created before"
msgstr "создан до"
#: documents/models.py:382
msgid "created after"
msgstr "создан после"
#: documents/models.py:383
msgid "created year is"
msgstr "год создания"
#: documents/models.py:384
msgid "created month is"
msgstr "месяц создания"
#: documents/models.py:385
msgid "created day is"
msgstr "день создания"
#: documents/models.py:386
msgid "added before"
msgstr "добавлен до"
#: documents/models.py:387
msgid "added after"
msgstr "добавлен после"
#: documents/models.py:388
msgid "modified before"
msgstr "изменен до"
#: documents/models.py:389
msgid "modified after"
msgstr "изменен после"
#: documents/models.py:390
msgid "does not have tag"
msgstr "не имеет тега"
#: documents/models.py:391
msgid "does not have ASN"
msgstr "не имеет архивного номера"
#: documents/models.py:392
msgid "title or content contains"
msgstr "Название или содержимое включает"
#: documents/models.py:393
msgid "fulltext query"
msgstr "полнотекстовый запрос"
#: documents/models.py:394
msgid "more like this"
msgstr "больше похожих"
#: documents/models.py:405
msgid "rule type"
msgstr "Тип правила"
#: documents/models.py:409
msgid "value"
msgstr "значение"
#: documents/models.py:415
msgid "filter rule"
msgstr "Правило фильтрации"
#: documents/models.py:416
msgid "filter rules"
msgstr "правила фильтрации"
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "неверное регулярное выражение: %(error)s"
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr "Неверный цвет."
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "Тип файла %(type)s не поддерживается"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "Paperless-ng загружается..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "Выполнен выход из Paperless-ng"
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "Вы успешно вышли из системы. До свидания!"
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "Войти снова"
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "Выполнен выход в Paperless-ng"
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "Пожалуйста, войдите."
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr "Неправильные имя пользователя или пароль! Попробуйте еще раз."
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "Имя пользователя"
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "Пароль"
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "Вход"
#: paperless/settings.py:303
msgid "English (US)"
msgstr "Английский (США)"
#: paperless/settings.py:304
msgid "English (GB)"
msgstr "Английский (Великобритании)"
#: paperless/settings.py:305
msgid "German"
msgstr "Немецкий"
#: paperless/settings.py:306
msgid "Dutch"
msgstr "Датский"
#: paperless/settings.py:307
msgid "French"
msgstr "Французский"
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr "Portuguese (Brazil)"
#: paperless/settings.py:309
msgid "Portuguese"
msgstr "Португальский"
#: paperless/settings.py:310
msgid "Italian"
msgstr "Italian"
#: paperless/settings.py:311
msgid "Romanian"
msgstr "Romanian"
#: paperless/settings.py:312
msgid "Russian"
msgstr "Русский"
#: paperless/settings.py:313
msgid "Spanish"
msgstr "Испанский"
#: paperless/settings.py:314
msgid "Polish"
msgstr ""
#: paperless/settings.py:315
msgid "Swedish"
msgstr ""
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "Администрирование Paperless-ng"
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr ""
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr ""
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Фильтр"
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless будет обрабатывать только те письма, которые соответствуют всем фильтрам, указанным ниже."
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Действия"
#: paperless_mail/admin.py:51
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:58
msgid "Metadata"
msgstr "Метаданные"
#: paperless_mail/admin.py:60
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 все равно будут обрабатывать все соответствующие правила, которые вы определили."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Безбумажная почта"
#: paperless_mail/models.py:11
msgid "mail account"
msgstr "почтовый ящик"
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr "Почтовые ящики"
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr "Без шифрования"
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr "Использовать SSL"
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr "Использовать STARTTLS"
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr "Сервер IMAP"
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr "Порт IMAP"
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "Обычно это 143 для нешифрованных и STARTTLS соединений и 993 для SSL-соединений."
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr "Безопасность IMAP"
#: paperless_mail/models.py:46
msgid "username"
msgstr "Имя пользователя"
#: paperless_mail/models.py:50
msgid "password"
msgstr "пароль"
#: paperless_mail/models.py:54
msgid "character set"
msgstr ""
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr ""
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "правило почты"
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "правила почты"
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "Обрабатывать только вложения."
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "Обрабатывать все файлы, включая 'встроенные' вложения."
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "Пометить как прочитанное, не обрабатывать прочитанные письма"
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "Пометить почту, не обрабатывать помеченные письма"
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "Переместить в указанную папку"
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Удалить"
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "Тема в качестве заголовка"
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "Использовать имя вложенного файла как заголовок"
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "Не назначать корреспондента"
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "Использовать email адрес"
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "Использовать имя (или адрес электронной почты, если недоступно)"
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "Использовать корреспондента, выбранного ниже"
#: paperless_mail/models.py:121
msgid "order"
msgstr "порядок"
#: paperless_mail/models.py:128
msgid "account"
msgstr "Учётная запись"
#: paperless_mail/models.py:132
msgid "folder"
msgstr "каталог"
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr ""
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "фильтр по отправителю"
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "фильтр по теме"
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "фильтр по тексту сообщения"
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "фильтр по имени вложения"
#: paperless_mail/models.py:150
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:156
msgid "maximum age"
msgstr "Максимальный возраст"
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "Указывается в днях."
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "Тип вложения"
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "Вложенные вложения включая встраиваемые изображения. Лучше совместить эту опцию с фильтром по имени вложения."
#: paperless_mail/models.py:169
msgid "action"
msgstr "действие"
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "параметр действия"
#: paperless_mail/models.py:177
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:184
msgid "assign title from"
msgstr "назначить заголовок из"
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "назначить этот тег"
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "назначить этот тип документа"
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "назначить корреспондента из"
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "назначить этого корреспондента"

View File

@@ -0,0 +1,698 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-07-30 18:02\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\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-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: sv-SE\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Dokument"
#: documents/models.py:32
msgid "Any word"
msgstr "Valfritt ord"
#: documents/models.py:33
msgid "All words"
msgstr "Alla ord"
#: documents/models.py:34
msgid "Exact match"
msgstr "Exakt matchning"
#: documents/models.py:35
msgid "Regular expression"
msgstr "Reguljära uttryck"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Ungefärligt ord"
#: documents/models.py:37
msgid "Automatic"
msgstr "Automatisk"
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "namn"
#: documents/models.py:45
msgid "match"
msgstr "träff"
#: documents/models.py:49
msgid "matching algorithm"
msgstr "matchande algoritm"
#: documents/models.py:55
msgid "is insensitive"
msgstr "är ej skiftlägeskänsligt"
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "korrespondent"
#: documents/models.py:75
msgid "correspondents"
msgstr "korrespondenter"
#: documents/models.py:81
msgid "color"
msgstr "färg"
#: documents/models.py:87
msgid "is inbox tag"
msgstr "är inkorgsetikett"
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "Markerar denna etikett som en inkorgsetikett: Alla nyligen konsumerade dokument kommer att märkas med inkorgsetiketter."
#: documents/models.py:94
msgid "tag"
msgstr "etikett"
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "etiketter"
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "dokumenttyp"
#: documents/models.py:102
msgid "document types"
msgstr "dokumenttyper"
#: documents/models.py:110
msgid "Unencrypted"
msgstr "Okrypterad"
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "Krypterad med GNU Privacy Guard"
#: documents/models.py:124
msgid "title"
msgstr "titel"
#: documents/models.py:137
msgid "content"
msgstr "innehåll"
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "Dokumentets obearbetade textdata. Detta fält används främst för sökning."
#: documents/models.py:144
msgid "mime type"
msgstr "MIME-typ"
#: documents/models.py:155
msgid "checksum"
msgstr "kontrollsumma"
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "Kontrollsumman för originaldokumentet."
#: documents/models.py:163
msgid "archive checksum"
msgstr "arkivera kontrollsumma"
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "Kontrollsumman för det arkiverade dokumentet."
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "skapad"
#: documents/models.py:176
msgid "modified"
msgstr "ändrad"
#: documents/models.py:180
msgid "storage type"
msgstr "lagringstyp"
#: documents/models.py:188
msgid "added"
msgstr "tillagd"
#: documents/models.py:192
msgid "filename"
msgstr "filnamn"
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "Nuvarande filnamn i lagringsutrymmet"
#: documents/models.py:202
msgid "archive filename"
msgstr "arkivfilnamn"
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr "Nuvarande arkivfilnamn i lagringsutrymmet"
#: documents/models.py:212
msgid "archive serial number"
msgstr "serienummer (arkivering)"
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr "Placeringen av detta dokument i ditt fysiska dokumentarkiv."
#: documents/models.py:223
msgid "document"
msgstr "dokument"
#: documents/models.py:224
msgid "documents"
msgstr "dokument"
#: documents/models.py:311
msgid "debug"
msgstr "felsök"
#: documents/models.py:312
msgid "information"
msgstr "information"
#: documents/models.py:313
msgid "warning"
msgstr "varning"
#: documents/models.py:314
msgid "error"
msgstr "fel"
#: documents/models.py:315
msgid "critical"
msgstr "kritisk"
#: documents/models.py:319
msgid "group"
msgstr "grupp"
#: documents/models.py:322
msgid "message"
msgstr "meddelande"
#: documents/models.py:325
msgid "level"
msgstr "nivå"
#: documents/models.py:332
msgid "log"
msgstr "logg"
#: documents/models.py:333
msgid "logs"
msgstr "loggar"
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "sparad vy"
#: documents/models.py:345
msgid "saved views"
msgstr "sparade vyer"
#: documents/models.py:348
msgid "user"
msgstr "användare"
#: documents/models.py:354
msgid "show on dashboard"
msgstr "visa på kontrollpanelen"
#: documents/models.py:357
msgid "show in sidebar"
msgstr "visa i sidofältet"
#: documents/models.py:361
msgid "sort field"
msgstr "sortera fält"
#: documents/models.py:367
msgid "sort reverse"
msgstr "sortera omvänt"
#: documents/models.py:373
msgid "title contains"
msgstr "titel innehåller"
#: documents/models.py:374
msgid "content contains"
msgstr "innehåll innehåller"
#: documents/models.py:375
msgid "ASN is"
msgstr "ASN är"
#: documents/models.py:376
msgid "correspondent is"
msgstr "korrespondent är"
#: documents/models.py:377
msgid "document type is"
msgstr "dokumenttyp är"
#: documents/models.py:378
msgid "is in inbox"
msgstr "är i inkorgen"
#: documents/models.py:379
msgid "has tag"
msgstr "har etikett"
#: documents/models.py:380
msgid "has any tag"
msgstr "har någon etikett"
#: documents/models.py:381
msgid "created before"
msgstr "skapad före"
#: documents/models.py:382
msgid "created after"
msgstr "skapad efter"
#: documents/models.py:383
msgid "created year is"
msgstr "skapat år är"
#: documents/models.py:384
msgid "created month is"
msgstr "skapad månad är"
#: documents/models.py:385
msgid "created day is"
msgstr "skapad dag är"
#: documents/models.py:386
msgid "added before"
msgstr "tillagd före"
#: documents/models.py:387
msgid "added after"
msgstr "tillagd efter"
#: documents/models.py:388
msgid "modified before"
msgstr "ändrad före"
#: documents/models.py:389
msgid "modified after"
msgstr "ändrad efter"
#: documents/models.py:390
msgid "does not have tag"
msgstr "har inte etikett"
#: documents/models.py:391
msgid "does not have ASN"
msgstr "har inte ASN"
#: documents/models.py:392
msgid "title or content contains"
msgstr "titel eller innehåll innehåller"
#: documents/models.py:393
msgid "fulltext query"
msgstr "fulltextfråga"
#: documents/models.py:394
msgid "more like this"
msgstr "mer som detta"
#: documents/models.py:405
msgid "rule type"
msgstr "regeltyp"
#: documents/models.py:409
msgid "value"
msgstr "värde"
#: documents/models.py:415
msgid "filter rule"
msgstr "filtrera regel"
#: documents/models.py:416
msgid "filter rules"
msgstr "filtrera regler"
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "Ogiltigt reguljärt uttryck: %(error)s"
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr "Ogiltig färg."
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "Filtypen %(type)s stöds inte"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "Paperless-ng laddas..."
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "Paperless-ng utloggad"
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "Du är nu utloggad. Hejdå!"
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "Logga in igen"
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "Paperless-ng inloggning"
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "Vänligen logga in."
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr "Ditt användarnamn och lösenord matchade inte. Vänligen försök igen."
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "Användarnamn"
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "Lösenord"
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "Logga in"
#: paperless/settings.py:303
msgid "English (US)"
msgstr "Engelska (USA)"
#: paperless/settings.py:304
msgid "English (GB)"
msgstr "Engelska (GB)"
#: paperless/settings.py:305
msgid "German"
msgstr "Tyska"
#: paperless/settings.py:306
msgid "Dutch"
msgstr "Holländska"
#: paperless/settings.py:307
msgid "French"
msgstr "Franska"
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr "Portugisiska (Brasilien)"
#: paperless/settings.py:309
msgid "Portuguese"
msgstr "Portugisiska"
#: paperless/settings.py:310
msgid "Italian"
msgstr "Italienska"
#: paperless/settings.py:311
msgid "Romanian"
msgstr "Rumänska"
#: paperless/settings.py:312
msgid "Russian"
msgstr "Ryska"
#: paperless/settings.py:313
msgid "Spanish"
msgstr "Spanska"
#: paperless/settings.py:314
msgid "Polish"
msgstr "Polska"
#: paperless/settings.py:315
msgid "Swedish"
msgstr "Svenska"
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "Paperless-ng administration"
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr "Autentisering"
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr "Avancerade inställningar"
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "Filter"
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "Paperless kommer endast att behandla e-postmeddelanden som matchar ALLA filter som anges nedan."
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "Åtgärder"
#: paperless_mail/admin.py:51
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 "Åtgärden tillämpas på e-postmeddelandet. Denna åtgärd utförs endast när dokument konsumerades från e-postmeddelandet. E-post utan bilagor kommer att förbli helt orörda."
#: paperless_mail/admin.py:58
msgid "Metadata"
msgstr "Metadata"
#: paperless_mail/admin.py:60
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 "Tilldela metadata till dokument som konsumeras från denna regel automatiskt. Om du inte tilldelar etiketter, typer eller korrespondenter här kommer paperless fortfarande att behandla alla matchande regler som du har definierat."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Paperless e-post"
#: paperless_mail/models.py:11
msgid "mail account"
msgstr "e-postkonto"
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr "e-postkonton"
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr "Ingen kryptering"
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr "Använd SSL"
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr "Använd STARTTLS"
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr "IMAP-server"
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr "IMAP-port"
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "Detta är vanligtvis 143 för okrypterade och STARTTLS-anslutningar, och 993 för SSL-anslutningar."
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr "IMAP-säkerhet"
#: paperless_mail/models.py:46
msgid "username"
msgstr "användarnamn"
#: paperless_mail/models.py:50
msgid "password"
msgstr "lösenord"
#: paperless_mail/models.py:54
msgid "character set"
msgstr "Teckenuppsättning"
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr "Teckenuppsättningen som är tänkt att användas vid kommunikation med mailservern, exempelvis UTF-8 eller US-ASCII."
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "e-postregel"
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "e-postregler"
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "Behandla endast bilagor."
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "Behandla alla filer, inklusive infogade bilagor."
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "Markera som läst, bearbeta inte lästa meddelanden"
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "Flagga meddelandet, bearbeta inte flaggade meddelanden"
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "Flytta till angiven mapp"
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "Radera"
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "Använd ämne som titel"
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "Använd bilagans filnamn som titel"
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "Tilldela inte en korrespondent"
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "Använd e-postadress"
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "Använd namn (eller e-postadress om inte tillgängligt)"
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "Använd korrespondent som valts nedan"
#: paperless_mail/models.py:121
msgid "order"
msgstr "ordning"
#: paperless_mail/models.py:128
msgid "account"
msgstr "konto"
#: paperless_mail/models.py:132
msgid "folder"
msgstr "mapp"
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr "Undermappar måste vara separerade med punkter."
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "filtrera från"
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "filtrera ämne"
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "filtrera kropp"
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "filtrera filnamn för bilaga"
#: paperless_mail/models.py:150
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "Konsumera endast dokument som matchar exakt detta filnamn, om det är angivet. Jokertecken som *.pdf eller *faktura* är tillåtna. Ej skiftlägeskänsligt."
#: paperless_mail/models.py:156
msgid "maximum age"
msgstr "högsta ålder"
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "Anges i dagar."
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "typ av bilaga"
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "Infogade bilagor inkluderar inbäddade bilder, så det är bäst att kombinera detta alternativ med ett filnamnsfilter."
#: paperless_mail/models.py:169
msgid "action"
msgstr "åtgärd"
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "åtgärdsparameter"
#: paperless_mail/models.py:177
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 "Ytterligare parametrar för åtgärden som valts ovan, d.v.s. målmappen för åtgärden \"flytta till angiven mapp\". Undermappar måste vara separerade med punkter."
#: paperless_mail/models.py:184
msgid "assign title from"
msgstr "tilldela titel från"
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "tilldela denna etikett"
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "tilldela den här dokumenttypen"
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "tilldela korrespondent från"
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "tilldela denna korrespondent"

View File

@@ -1,21 +1,21 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-10 21:41+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"POT-Creation-Date: 2021-02-28 12:40+0100\n"
"PO-Revision-Date: 2021-03-06 21:39\n"
"Last-Translator: \n"
"Language-Team: Thai\n"
"Language: th_TH\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Crowdin-Project: paperless-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: th\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
@@ -45,7 +45,7 @@ msgstr ""
msgid "Automatic"
msgstr ""
#: documents/models.py:41 documents/models.py:354 paperless_mail/models.py:25
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:109
msgid "name"
msgstr ""
@@ -62,283 +62,301 @@ msgstr ""
msgid "is insensitive"
msgstr ""
#: documents/models.py:80 documents/models.py:140
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr ""
#: documents/models.py:81
#: documents/models.py:75
msgid "correspondents"
msgstr ""
#: documents/models.py:103
#: documents/models.py:81
msgid "color"
msgstr ""
#: documents/models.py:107
#: documents/models.py:87
msgid "is inbox tag"
msgstr ""
#: documents/models.py:109
msgid ""
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
"with inbox tags."
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr ""
#: documents/models.py:114
#: documents/models.py:94
msgid "tag"
msgstr ""
#: documents/models.py:115 documents/models.py:171
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr ""
#: documents/models.py:121 documents/models.py:153
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr ""
#: documents/models.py:122
#: documents/models.py:102
msgid "document types"
msgstr ""
#: documents/models.py:130
#: documents/models.py:110
msgid "Unencrypted"
msgstr ""
#: documents/models.py:131
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr ""
#: documents/models.py:144
#: documents/models.py:124
msgid "title"
msgstr ""
#: documents/models.py:157
#: documents/models.py:137
msgid "content"
msgstr ""
#: documents/models.py:159
msgid ""
"The raw, text-only data of the document. This field is primarily used for "
"searching."
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr ""
#: documents/models.py:164
#: documents/models.py:144
msgid "mime type"
msgstr ""
#: documents/models.py:175
#: documents/models.py:155
msgid "checksum"
msgstr ""
#: documents/models.py:179
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr ""
#: documents/models.py:183
#: documents/models.py:163
msgid "archive checksum"
msgstr ""
#: documents/models.py:188
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr ""
#: documents/models.py:192 documents/models.py:332
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr ""
#: documents/models.py:196
#: documents/models.py:176
msgid "modified"
msgstr ""
#: documents/models.py:200
#: documents/models.py:180
msgid "storage type"
msgstr ""
#: documents/models.py:208
#: documents/models.py:188
msgid "added"
msgstr ""
#: documents/models.py:212
#: documents/models.py:192
msgid "filename"
msgstr ""
#: documents/models.py:217
#: documents/models.py:198
msgid "Current filename in storage"
msgstr ""
#: documents/models.py:221
#: documents/models.py:202
msgid "archive filename"
msgstr ""
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr ""
#: documents/models.py:212
msgid "archive serial number"
msgstr ""
#: documents/models.py:226
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr ""
#: documents/models.py:232
#: documents/models.py:223
msgid "document"
msgstr ""
#: documents/models.py:233
#: documents/models.py:224
msgid "documents"
msgstr ""
#: documents/models.py:315
#: documents/models.py:311
msgid "debug"
msgstr ""
#: documents/models.py:316
#: documents/models.py:312
msgid "information"
msgstr ""
#: documents/models.py:317
#: documents/models.py:313
msgid "warning"
msgstr ""
#: documents/models.py:318
#: documents/models.py:314
msgid "error"
msgstr ""
#: documents/models.py:319
#: documents/models.py:315
msgid "critical"
msgstr ""
#: documents/models.py:323
#: documents/models.py:319
msgid "group"
msgstr ""
#: documents/models.py:326
#: documents/models.py:322
msgid "message"
msgstr ""
#: documents/models.py:329
#: documents/models.py:325
msgid "level"
msgstr ""
#: documents/models.py:336
#: documents/models.py:332
msgid "log"
msgstr ""
#: documents/models.py:337
#: documents/models.py:333
msgid "logs"
msgstr ""
#: documents/models.py:348 documents/models.py:398
#: documents/models.py:344 documents/models.py:394
msgid "saved view"
msgstr ""
#: documents/models.py:349
#: documents/models.py:345
msgid "saved views"
msgstr ""
#: documents/models.py:352
#: documents/models.py:348
msgid "user"
msgstr ""
#: documents/models.py:358
#: documents/models.py:354
msgid "show on dashboard"
msgstr ""
#: documents/models.py:361
#: documents/models.py:357
msgid "show in sidebar"
msgstr ""
#: documents/models.py:365
#: documents/models.py:361
msgid "sort field"
msgstr ""
#: documents/models.py:368
#: documents/models.py:364
msgid "sort reverse"
msgstr ""
#: documents/models.py:374
#: documents/models.py:370
msgid "title contains"
msgstr ""
#: documents/models.py:375
#: documents/models.py:371
msgid "content contains"
msgstr ""
#: documents/models.py:376
#: documents/models.py:372
msgid "ASN is"
msgstr ""
#: documents/models.py:377
#: documents/models.py:373
msgid "correspondent is"
msgstr ""
#: documents/models.py:378
#: documents/models.py:374
msgid "document type is"
msgstr ""
#: documents/models.py:379
#: documents/models.py:375
msgid "is in inbox"
msgstr ""
#: documents/models.py:380
#: documents/models.py:376
msgid "has tag"
msgstr ""
#: documents/models.py:381
#: documents/models.py:377
msgid "has any tag"
msgstr ""
#: documents/models.py:382
#: documents/models.py:378
msgid "created before"
msgstr ""
#: documents/models.py:383
#: documents/models.py:379
msgid "created after"
msgstr ""
#: documents/models.py:384
#: documents/models.py:380
msgid "created year is"
msgstr ""
#: documents/models.py:385
#: documents/models.py:381
msgid "created month is"
msgstr ""
#: documents/models.py:386
#: documents/models.py:382
msgid "created day is"
msgstr ""
#: documents/models.py:387
#: documents/models.py:383
msgid "added before"
msgstr ""
#: documents/models.py:388
#: documents/models.py:384
msgid "added after"
msgstr ""
#: documents/models.py:389
#: documents/models.py:385
msgid "modified before"
msgstr ""
#: documents/models.py:390
#: documents/models.py:386
msgid "modified after"
msgstr ""
#: documents/models.py:391
#: documents/models.py:387
msgid "does not have tag"
msgstr ""
#: documents/models.py:402
#: documents/models.py:398
msgid "rule type"
msgstr ""
#: documents/models.py:406
#: documents/models.py:402
msgid "value"
msgstr ""
#: documents/models.py:412
#: documents/models.py:408
msgid "filter rule"
msgstr ""
#: documents/models.py:413
#: documents/models.py:409
msgid "filter rules"
msgstr ""
#: documents/templates/index.html:20
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expresssion: %(error)s"
msgstr ""
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr ""
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
#: documents/templates/index.html:21
msgid "Paperless-ng is loading..."
msgstr ""
@@ -378,23 +396,39 @@ msgstr ""
msgid "Sign in"
msgstr ""
#: paperless/settings.py:268
msgid "English"
#: paperless/settings.py:297
msgid "English (US)"
msgstr ""
#: paperless/settings.py:269
#: paperless/settings.py:298
msgid "English (GB)"
msgstr ""
#: paperless/settings.py:299
msgid "German"
msgstr ""
#: paperless/settings.py:270
#: paperless/settings.py:300
msgid "Dutch"
msgstr ""
#: paperless/settings.py:271
#: paperless/settings.py:301
msgid "French"
msgstr ""
#: paperless/urls.py:108
#: paperless/settings.py:302
msgid "Portuguese (Brazil)"
msgstr ""
#: paperless/settings.py:303
msgid "Italian"
msgstr ""
#: paperless/settings.py:304
msgid "Romanian"
msgstr ""
#: paperless/urls.py:118
msgid "Paperless-ng administration"
msgstr ""
@@ -403,8 +437,7 @@ msgid "Filter"
msgstr ""
#: paperless_mail/admin.py:27
msgid ""
"Paperless will only process mails that match ALL of the filters given below."
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr ""
#: paperless_mail/admin.py:37
@@ -412,10 +445,7 @@ msgid "Actions"
msgstr ""
#: paperless_mail/admin.py:39
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."
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:46
@@ -423,10 +453,7 @@ msgid "Metadata"
msgstr ""
#: paperless_mail/admin.py:48
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."
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_mail/apps.py:9
@@ -462,9 +489,7 @@ msgid "IMAP port"
msgstr ""
#: paperless_mail/models.py:36
msgid ""
"This is usually 143 for unencrypted and STARTTLS connections, and 993 for "
"SSL connections."
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr ""
#: paperless_mail/models.py:40
@@ -564,9 +589,7 @@ msgid "filter attachment filename"
msgstr ""
#: paperless_mail/models.py:140
msgid ""
"Only consume documents which entirely match this filename if specified. "
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
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:146
@@ -582,9 +605,7 @@ msgid "attachment type"
msgstr ""
#: paperless_mail/models.py:154
msgid ""
"Inline attachments include embedded images, so it's best to combine this "
"option with a filename filter."
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr ""
#: paperless_mail/models.py:159
@@ -596,9 +617,7 @@ msgid "action parameter"
msgstr ""
#: paperless_mail/models.py:167
msgid ""
"Additional parameter for the action selected above, i.e., the target folder "
"of the move to folder action."
msgid "Additional parameter for the action selected above, i.e., the target folder of the move to folder action."
msgstr ""
#: paperless_mail/models.py:173
@@ -620,3 +639,4 @@ msgstr ""
#: paperless_mail/models.py:205
msgid "assign this correspondent"
msgstr ""

View File

@@ -0,0 +1,698 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-05-16 10:09\n"
"Last-Translator: \n"
"Language-Team: Xhosa\n"
"Language: xh_ZA\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-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: xh\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "crwdns2528:0crwdne2528:0"
#: documents/models.py:32
msgid "Any word"
msgstr "crwdns2530:0crwdne2530:0"
#: documents/models.py:33
msgid "All words"
msgstr "crwdns2532:0crwdne2532:0"
#: documents/models.py:34
msgid "Exact match"
msgstr "crwdns2534:0crwdne2534:0"
#: documents/models.py:35
msgid "Regular expression"
msgstr "crwdns2536:0crwdne2536:0"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "crwdns2538:0crwdne2538:0"
#: documents/models.py:37
msgid "Automatic"
msgstr "crwdns2540:0crwdne2540:0"
#: documents/models.py:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "crwdns2542:0crwdne2542:0"
#: documents/models.py:45
msgid "match"
msgstr "crwdns2544:0crwdne2544:0"
#: documents/models.py:49
msgid "matching algorithm"
msgstr "crwdns2546:0crwdne2546:0"
#: documents/models.py:55
msgid "is insensitive"
msgstr "crwdns2548:0crwdne2548:0"
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr "crwdns2550:0crwdne2550:0"
#: documents/models.py:75
msgid "correspondents"
msgstr "crwdns2552:0crwdne2552:0"
#: documents/models.py:81
msgid "color"
msgstr "crwdns2554:0crwdne2554:0"
#: documents/models.py:87
msgid "is inbox tag"
msgstr "crwdns2556:0crwdne2556:0"
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "crwdns2558:0crwdne2558:0"
#: documents/models.py:94
msgid "tag"
msgstr "crwdns2560:0crwdne2560:0"
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr "crwdns2562:0crwdne2562:0"
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr "crwdns2564:0crwdne2564:0"
#: documents/models.py:102
msgid "document types"
msgstr "crwdns2566:0crwdne2566:0"
#: documents/models.py:110
msgid "Unencrypted"
msgstr "crwdns2568:0crwdne2568:0"
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr "crwdns2570:0crwdne2570:0"
#: documents/models.py:124
msgid "title"
msgstr "crwdns2572:0crwdne2572:0"
#: documents/models.py:137
msgid "content"
msgstr "crwdns2574:0crwdne2574:0"
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr "crwdns2576:0crwdne2576:0"
#: documents/models.py:144
msgid "mime type"
msgstr "crwdns2578:0crwdne2578:0"
#: documents/models.py:155
msgid "checksum"
msgstr "crwdns2580:0crwdne2580:0"
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr "crwdns2582:0crwdne2582:0"
#: documents/models.py:163
msgid "archive checksum"
msgstr "crwdns2584:0crwdne2584:0"
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr "crwdns2586:0crwdne2586:0"
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr "crwdns2588:0crwdne2588:0"
#: documents/models.py:176
msgid "modified"
msgstr "crwdns2590:0crwdne2590:0"
#: documents/models.py:180
msgid "storage type"
msgstr "crwdns2592:0crwdne2592:0"
#: documents/models.py:188
msgid "added"
msgstr "crwdns2594:0crwdne2594:0"
#: documents/models.py:192
msgid "filename"
msgstr "crwdns2596:0crwdne2596:0"
#: documents/models.py:198
msgid "Current filename in storage"
msgstr "crwdns2598:0crwdne2598:0"
#: documents/models.py:202
msgid "archive filename"
msgstr "crwdns2600:0crwdne2600:0"
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr "crwdns2602:0crwdne2602:0"
#: documents/models.py:212
msgid "archive serial number"
msgstr "crwdns2604:0crwdne2604:0"
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr "crwdns2606:0crwdne2606:0"
#: documents/models.py:223
msgid "document"
msgstr "crwdns2608:0crwdne2608:0"
#: documents/models.py:224
msgid "documents"
msgstr "crwdns2610:0crwdne2610:0"
#: documents/models.py:311
msgid "debug"
msgstr "crwdns2612:0crwdne2612:0"
#: documents/models.py:312
msgid "information"
msgstr "crwdns2614:0crwdne2614:0"
#: documents/models.py:313
msgid "warning"
msgstr "crwdns2616:0crwdne2616:0"
#: documents/models.py:314
msgid "error"
msgstr "crwdns2618:0crwdne2618:0"
#: documents/models.py:315
msgid "critical"
msgstr "crwdns2620:0crwdne2620:0"
#: documents/models.py:319
msgid "group"
msgstr "crwdns2622:0crwdne2622:0"
#: documents/models.py:322
msgid "message"
msgstr "crwdns2624:0crwdne2624:0"
#: documents/models.py:325
msgid "level"
msgstr "crwdns2626:0crwdne2626:0"
#: documents/models.py:332
msgid "log"
msgstr "crwdns2628:0crwdne2628:0"
#: documents/models.py:333
msgid "logs"
msgstr "crwdns2630:0crwdne2630:0"
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr "crwdns2632:0crwdne2632:0"
#: documents/models.py:345
msgid "saved views"
msgstr "crwdns2634:0crwdne2634:0"
#: documents/models.py:348
msgid "user"
msgstr "crwdns2636:0crwdne2636:0"
#: documents/models.py:354
msgid "show on dashboard"
msgstr "crwdns2638:0crwdne2638:0"
#: documents/models.py:357
msgid "show in sidebar"
msgstr "crwdns2640:0crwdne2640:0"
#: documents/models.py:361
msgid "sort field"
msgstr "crwdns2642:0crwdne2642:0"
#: documents/models.py:367
msgid "sort reverse"
msgstr "crwdns2644:0crwdne2644:0"
#: documents/models.py:373
msgid "title contains"
msgstr "crwdns2646:0crwdne2646:0"
#: documents/models.py:374
msgid "content contains"
msgstr "crwdns2648:0crwdne2648:0"
#: documents/models.py:375
msgid "ASN is"
msgstr "crwdns2650:0crwdne2650:0"
#: documents/models.py:376
msgid "correspondent is"
msgstr "crwdns2652:0crwdne2652:0"
#: documents/models.py:377
msgid "document type is"
msgstr "crwdns2654:0crwdne2654:0"
#: documents/models.py:378
msgid "is in inbox"
msgstr "crwdns2656:0crwdne2656:0"
#: documents/models.py:379
msgid "has tag"
msgstr "crwdns2658:0crwdne2658:0"
#: documents/models.py:380
msgid "has any tag"
msgstr "crwdns2660:0crwdne2660:0"
#: documents/models.py:381
msgid "created before"
msgstr "crwdns2662:0crwdne2662:0"
#: documents/models.py:382
msgid "created after"
msgstr "crwdns2664:0crwdne2664:0"
#: documents/models.py:383
msgid "created year is"
msgstr "crwdns2666:0crwdne2666:0"
#: documents/models.py:384
msgid "created month is"
msgstr "crwdns2668:0crwdne2668:0"
#: documents/models.py:385
msgid "created day is"
msgstr "crwdns2670:0crwdne2670:0"
#: documents/models.py:386
msgid "added before"
msgstr "crwdns2672:0crwdne2672:0"
#: documents/models.py:387
msgid "added after"
msgstr "crwdns2674:0crwdne2674:0"
#: documents/models.py:388
msgid "modified before"
msgstr "crwdns2676:0crwdne2676:0"
#: documents/models.py:389
msgid "modified after"
msgstr "crwdns2678:0crwdne2678:0"
#: documents/models.py:390
msgid "does not have tag"
msgstr "crwdns2680:0crwdne2680:0"
#: documents/models.py:391
msgid "does not have ASN"
msgstr "crwdns3408:0crwdne3408:0"
#: documents/models.py:392
msgid "title or content contains"
msgstr "crwdns3410:0crwdne3410:0"
#: documents/models.py:393
msgid "fulltext query"
msgstr "crwdns3438:0crwdne3438:0"
#: documents/models.py:394
msgid "more like this"
msgstr "crwdns3440:0crwdne3440:0"
#: documents/models.py:405
msgid "rule type"
msgstr "crwdns2682:0crwdne2682:0"
#: documents/models.py:409
msgid "value"
msgstr "crwdns2684:0crwdne2684:0"
#: documents/models.py:415
msgid "filter rule"
msgstr "crwdns2686:0crwdne2686:0"
#: documents/models.py:416
msgid "filter rules"
msgstr "crwdns2688:0crwdne2688:0"
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr "crwdns3412:0%(error)scrwdne3412:0"
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr "crwdns2692:0crwdne2692:0"
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr "crwdns2694:0%(type)scrwdne2694:0"
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr "crwdns2696:0crwdne2696:0"
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr "crwdns2698:0crwdne2698:0"
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr "crwdns2700:0crwdne2700:0"
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr "crwdns2702:0crwdne2702:0"
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr "crwdns2704:0crwdne2704:0"
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr "crwdns2706:0crwdne2706:0"
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr "crwdns2708:0crwdne2708:0"
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr "crwdns2710:0crwdne2710:0"
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr "crwdns2712:0crwdne2712:0"
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr "crwdns2714:0crwdne2714:0"
#: paperless/settings.py:303
msgid "English (US)"
msgstr "crwdns2716:0crwdne2716:0"
#: paperless/settings.py:304
msgid "English (GB)"
msgstr "crwdns2718:0crwdne2718:0"
#: paperless/settings.py:305
msgid "German"
msgstr "crwdns2720:0crwdne2720:0"
#: paperless/settings.py:306
msgid "Dutch"
msgstr "crwdns2722:0crwdne2722:0"
#: paperless/settings.py:307
msgid "French"
msgstr "crwdns2724:0crwdne2724:0"
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr "crwdns2726:0crwdne2726:0"
#: paperless/settings.py:309
msgid "Portuguese"
msgstr "crwdns3424:0crwdne3424:0"
#: paperless/settings.py:310
msgid "Italian"
msgstr "crwdns2728:0crwdne2728:0"
#: paperless/settings.py:311
msgid "Romanian"
msgstr "crwdns2730:0crwdne2730:0"
#: paperless/settings.py:312
msgid "Russian"
msgstr "crwdns3414:0crwdne3414:0"
#: paperless/settings.py:313
msgid "Spanish"
msgstr "crwdns3420:0crwdne3420:0"
#: paperless/settings.py:314
msgid "Polish"
msgstr "crwdns3444:0crwdne3444:0"
#: paperless/settings.py:315
msgid "Swedish"
msgstr "crwdns3448:0crwdne3448:0"
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr "crwdns2732:0crwdne2732:0"
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr "crwdns3456:0crwdne3456:0"
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr "crwdns3458:0crwdne3458:0"
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr "crwdns2734:0crwdne2734:0"
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr "crwdns2736:0crwdne2736:0"
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr "crwdns2738:0crwdne2738:0"
#: paperless_mail/admin.py:51
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 "crwdns2740:0crwdne2740:0"
#: paperless_mail/admin.py:58
msgid "Metadata"
msgstr "crwdns2742:0crwdne2742:0"
#: paperless_mail/admin.py:60
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 "crwdns2744:0crwdne2744:0"
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "crwdns2746:0crwdne2746:0"
#: paperless_mail/models.py:11
msgid "mail account"
msgstr "crwdns2748:0crwdne2748:0"
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr "crwdns2750:0crwdne2750:0"
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr "crwdns2752:0crwdne2752:0"
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr "crwdns2754:0crwdne2754:0"
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr "crwdns2756:0crwdne2756:0"
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr "crwdns2758:0crwdne2758:0"
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr "crwdns2760:0crwdne2760:0"
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr "crwdns2762:0crwdne2762:0"
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr "crwdns2764:0crwdne2764:0"
#: paperless_mail/models.py:46
msgid "username"
msgstr "crwdns2766:0crwdne2766:0"
#: paperless_mail/models.py:50
msgid "password"
msgstr "crwdns2768:0crwdne2768:0"
#: paperless_mail/models.py:54
msgid "character set"
msgstr "crwdns3460:0crwdne3460:0"
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr "crwdns3462:0crwdne3462:0"
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr "crwdns2770:0crwdne2770:0"
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr "crwdns2772:0crwdne2772:0"
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr "crwdns2774:0crwdne2774:0"
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr "crwdns2776:0crwdne2776:0"
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr "crwdns2778:0crwdne2778:0"
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr "crwdns2780:0crwdne2780:0"
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr "crwdns2782:0crwdne2782:0"
#: paperless_mail/models.py:89
msgid "Delete"
msgstr "crwdns2784:0crwdne2784:0"
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr "crwdns2786:0crwdne2786:0"
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr "crwdns2788:0crwdne2788:0"
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr "crwdns2790:0crwdne2790:0"
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr "crwdns2792:0crwdne2792:0"
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr "crwdns2794:0crwdne2794:0"
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr "crwdns2796:0crwdne2796:0"
#: paperless_mail/models.py:121
msgid "order"
msgstr "crwdns2798:0crwdne2798:0"
#: paperless_mail/models.py:128
msgid "account"
msgstr "crwdns2800:0crwdne2800:0"
#: paperless_mail/models.py:132
msgid "folder"
msgstr "crwdns2802:0crwdne2802:0"
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr "crwdns3464:0crwdne3464:0"
#: paperless_mail/models.py:138
msgid "filter from"
msgstr "crwdns2804:0crwdne2804:0"
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr "crwdns2806:0crwdne2806:0"
#: paperless_mail/models.py:144
msgid "filter body"
msgstr "crwdns2808:0crwdne2808:0"
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr "crwdns2810:0crwdne2810:0"
#: paperless_mail/models.py:150
msgid "Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr "crwdns2812:0crwdne2812:0"
#: paperless_mail/models.py:156
msgid "maximum age"
msgstr "crwdns2814:0crwdne2814:0"
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr "crwdns2816:0crwdne2816:0"
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr "crwdns2818:0crwdne2818:0"
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr "crwdns2820:0crwdne2820:0"
#: paperless_mail/models.py:169
msgid "action"
msgstr "crwdns2822:0crwdne2822:0"
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr "crwdns2824:0crwdne2824:0"
#: paperless_mail/models.py:177
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 "crwdns3466:0crwdne3466:0"
#: paperless_mail/models.py:184
msgid "assign title from"
msgstr "crwdns2828:0crwdne2828:0"
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr "crwdns2830:0crwdne2830:0"
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr "crwdns2832:0crwdne2832:0"
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr "crwdns2834:0crwdne2834:0"
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr "crwdns2836:0crwdne2836:0"

View File

@@ -0,0 +1,698 @@
msgid ""
msgstr ""
"Project-Id-Version: paperless-ng\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-16 09:38+0000\n"
"PO-Revision-Date: 2021-05-16 10:09\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Crowdin-Project: paperless-ng\n"
"X-Crowdin-Project-ID: 434940\n"
"X-Crowdin-Language: zh-CN\n"
"X-Crowdin-File: /dev/src/locale/en_US/LC_MESSAGES/django.po\n"
"X-Crowdin-File-ID: 54\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:41 documents/models.py:350 paperless_mail/models.py:25
#: paperless_mail/models.py:117
msgid "name"
msgstr "名字"
#: documents/models.py:45
msgid "match"
msgstr "配对"
#: documents/models.py:49
msgid "matching algorithm"
msgstr "配对算法"
#: documents/models.py:55
msgid "is insensitive"
msgstr ""
#: documents/models.py:74 documents/models.py:120
msgid "correspondent"
msgstr ""
#: documents/models.py:75
msgid "correspondents"
msgstr ""
#: documents/models.py:81
msgid "color"
msgstr ""
#: documents/models.py:87
msgid "is inbox tag"
msgstr ""
#: documents/models.py:89
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr ""
#: documents/models.py:94
msgid "tag"
msgstr ""
#: documents/models.py:95 documents/models.py:151
msgid "tags"
msgstr ""
#: documents/models.py:101 documents/models.py:133
msgid "document type"
msgstr ""
#: documents/models.py:102
msgid "document types"
msgstr ""
#: documents/models.py:110
msgid "Unencrypted"
msgstr ""
#: documents/models.py:111
msgid "Encrypted with GNU Privacy Guard"
msgstr ""
#: documents/models.py:124
msgid "title"
msgstr ""
#: documents/models.py:137
msgid "content"
msgstr ""
#: documents/models.py:139
msgid "The raw, text-only data of the document. This field is primarily used for searching."
msgstr ""
#: documents/models.py:144
msgid "mime type"
msgstr ""
#: documents/models.py:155
msgid "checksum"
msgstr ""
#: documents/models.py:159
msgid "The checksum of the original document."
msgstr ""
#: documents/models.py:163
msgid "archive checksum"
msgstr ""
#: documents/models.py:168
msgid "The checksum of the archived document."
msgstr ""
#: documents/models.py:172 documents/models.py:328
msgid "created"
msgstr ""
#: documents/models.py:176
msgid "modified"
msgstr ""
#: documents/models.py:180
msgid "storage type"
msgstr ""
#: documents/models.py:188
msgid "added"
msgstr ""
#: documents/models.py:192
msgid "filename"
msgstr ""
#: documents/models.py:198
msgid "Current filename in storage"
msgstr ""
#: documents/models.py:202
msgid "archive filename"
msgstr ""
#: documents/models.py:208
msgid "Current archive filename in storage"
msgstr ""
#: documents/models.py:212
msgid "archive serial number"
msgstr ""
#: documents/models.py:217
msgid "The position of this document in your physical document archive."
msgstr ""
#: documents/models.py:223
msgid "document"
msgstr ""
#: documents/models.py:224
msgid "documents"
msgstr ""
#: documents/models.py:311
msgid "debug"
msgstr ""
#: documents/models.py:312
msgid "information"
msgstr ""
#: documents/models.py:313
msgid "warning"
msgstr ""
#: documents/models.py:314
msgid "error"
msgstr ""
#: documents/models.py:315
msgid "critical"
msgstr ""
#: documents/models.py:319
msgid "group"
msgstr ""
#: documents/models.py:322
msgid "message"
msgstr ""
#: documents/models.py:325
msgid "level"
msgstr ""
#: documents/models.py:332
msgid "log"
msgstr ""
#: documents/models.py:333
msgid "logs"
msgstr ""
#: documents/models.py:344 documents/models.py:401
msgid "saved view"
msgstr ""
#: documents/models.py:345
msgid "saved views"
msgstr ""
#: documents/models.py:348
msgid "user"
msgstr ""
#: documents/models.py:354
msgid "show on dashboard"
msgstr ""
#: documents/models.py:357
msgid "show in sidebar"
msgstr ""
#: documents/models.py:361
msgid "sort field"
msgstr ""
#: documents/models.py:367
msgid "sort reverse"
msgstr ""
#: documents/models.py:373
msgid "title contains"
msgstr ""
#: documents/models.py:374
msgid "content contains"
msgstr ""
#: documents/models.py:375
msgid "ASN is"
msgstr ""
#: documents/models.py:376
msgid "correspondent is"
msgstr ""
#: documents/models.py:377
msgid "document type is"
msgstr ""
#: documents/models.py:378
msgid "is in inbox"
msgstr ""
#: documents/models.py:379
msgid "has tag"
msgstr ""
#: documents/models.py:380
msgid "has any tag"
msgstr ""
#: documents/models.py:381
msgid "created before"
msgstr ""
#: documents/models.py:382
msgid "created after"
msgstr ""
#: documents/models.py:383
msgid "created year is"
msgstr ""
#: documents/models.py:384
msgid "created month is"
msgstr ""
#: documents/models.py:385
msgid "created day is"
msgstr ""
#: documents/models.py:386
msgid "added before"
msgstr ""
#: documents/models.py:387
msgid "added after"
msgstr ""
#: documents/models.py:388
msgid "modified before"
msgstr ""
#: documents/models.py:389
msgid "modified after"
msgstr ""
#: documents/models.py:390
msgid "does not have tag"
msgstr ""
#: documents/models.py:391
msgid "does not have ASN"
msgstr ""
#: documents/models.py:392
msgid "title or content contains"
msgstr ""
#: documents/models.py:393
msgid "fulltext query"
msgstr ""
#: documents/models.py:394
msgid "more like this"
msgstr ""
#: documents/models.py:405
msgid "rule type"
msgstr ""
#: documents/models.py:409
msgid "value"
msgstr ""
#: documents/models.py:415
msgid "filter rule"
msgstr ""
#: documents/models.py:416
msgid "filter rules"
msgstr ""
#: documents/serialisers.py:53
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr ""
#: documents/serialisers.py:177
msgid "Invalid color."
msgstr ""
#: documents/serialisers.py:451
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
#: documents/templates/index.html:22
msgid "Paperless-ng is loading..."
msgstr ""
#: documents/templates/registration/logged_out.html:14
msgid "Paperless-ng signed out"
msgstr ""
#: documents/templates/registration/logged_out.html:45
msgid "You have been successfully logged out. Bye!"
msgstr ""
#: documents/templates/registration/logged_out.html:46
msgid "Sign in again"
msgstr ""
#: documents/templates/registration/login.html:15
msgid "Paperless-ng sign in"
msgstr ""
#: documents/templates/registration/login.html:47
msgid "Please sign in."
msgstr ""
#: documents/templates/registration/login.html:50
msgid "Your username and password didn't match. Please try again."
msgstr ""
#: documents/templates/registration/login.html:53
msgid "Username"
msgstr ""
#: documents/templates/registration/login.html:54
msgid "Password"
msgstr ""
#: documents/templates/registration/login.html:59
msgid "Sign in"
msgstr ""
#: paperless/settings.py:303
msgid "English (US)"
msgstr ""
#: paperless/settings.py:304
msgid "English (GB)"
msgstr ""
#: paperless/settings.py:305
msgid "German"
msgstr ""
#: paperless/settings.py:306
msgid "Dutch"
msgstr ""
#: paperless/settings.py:307
msgid "French"
msgstr ""
#: paperless/settings.py:308
msgid "Portuguese (Brazil)"
msgstr ""
#: paperless/settings.py:309
msgid "Portuguese"
msgstr ""
#: paperless/settings.py:310
msgid "Italian"
msgstr ""
#: paperless/settings.py:311
msgid "Romanian"
msgstr ""
#: paperless/settings.py:312
msgid "Russian"
msgstr ""
#: paperless/settings.py:313
msgid "Spanish"
msgstr ""
#: paperless/settings.py:314
msgid "Polish"
msgstr ""
#: paperless/settings.py:315
msgid "Swedish"
msgstr ""
#: paperless/urls.py:120
msgid "Paperless-ng administration"
msgstr ""
#: paperless_mail/admin.py:15
msgid "Authentication"
msgstr ""
#: paperless_mail/admin.py:18
msgid "Advanced settings"
msgstr ""
#: paperless_mail/admin.py:37
msgid "Filter"
msgstr ""
#: paperless_mail/admin.py:39
msgid "Paperless will only process mails that match ALL of the filters given below."
msgstr ""
#: paperless_mail/admin.py:49
msgid "Actions"
msgstr ""
#: paperless_mail/admin.py:51
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:58
msgid "Metadata"
msgstr ""
#: paperless_mail/admin.py:60
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_mail/apps.py:9
msgid "Paperless mail"
msgstr ""
#: paperless_mail/models.py:11
msgid "mail account"
msgstr ""
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr ""
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr ""
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr ""
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr ""
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr ""
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr ""
#: paperless_mail/models.py:36
msgid "This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections."
msgstr ""
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr ""
#: paperless_mail/models.py:46
msgid "username"
msgstr ""
#: paperless_mail/models.py:50
msgid "password"
msgstr ""
#: paperless_mail/models.py:54
msgid "character set"
msgstr ""
#: paperless_mail/models.py:57
msgid "The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'."
msgstr ""
#: paperless_mail/models.py:68
msgid "mail rule"
msgstr ""
#: paperless_mail/models.py:69
msgid "mail rules"
msgstr ""
#: paperless_mail/models.py:75
msgid "Only process attachments."
msgstr ""
#: paperless_mail/models.py:76
msgid "Process all files, including 'inline' attachments."
msgstr ""
#: paperless_mail/models.py:86
msgid "Mark as read, don't process read mails"
msgstr ""
#: paperless_mail/models.py:87
msgid "Flag the mail, don't process flagged mails"
msgstr ""
#: paperless_mail/models.py:88
msgid "Move to specified folder"
msgstr ""
#: paperless_mail/models.py:89
msgid "Delete"
msgstr ""
#: paperless_mail/models.py:96
msgid "Use subject as title"
msgstr ""
#: paperless_mail/models.py:97
msgid "Use attachment filename as title"
msgstr ""
#: paperless_mail/models.py:107
msgid "Do not assign a correspondent"
msgstr ""
#: paperless_mail/models.py:109
msgid "Use mail address"
msgstr ""
#: paperless_mail/models.py:111
msgid "Use name (or mail address if not available)"
msgstr ""
#: paperless_mail/models.py:113
msgid "Use correspondent selected below"
msgstr ""
#: paperless_mail/models.py:121
msgid "order"
msgstr ""
#: paperless_mail/models.py:128
msgid "account"
msgstr ""
#: paperless_mail/models.py:132
msgid "folder"
msgstr ""
#: paperless_mail/models.py:134
msgid "Subfolders must be separated by dots."
msgstr ""
#: paperless_mail/models.py:138
msgid "filter from"
msgstr ""
#: paperless_mail/models.py:141
msgid "filter subject"
msgstr ""
#: paperless_mail/models.py:144
msgid "filter body"
msgstr ""
#: paperless_mail/models.py:148
msgid "filter attachment filename"
msgstr ""
#: paperless_mail/models.py:150
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:156
msgid "maximum age"
msgstr ""
#: paperless_mail/models.py:158
msgid "Specified in days."
msgstr ""
#: paperless_mail/models.py:161
msgid "attachment type"
msgstr ""
#: paperless_mail/models.py:164
msgid "Inline attachments include embedded images, so it's best to combine this option with a filename filter."
msgstr ""
#: paperless_mail/models.py:169
msgid "action"
msgstr ""
#: paperless_mail/models.py:175
msgid "action parameter"
msgstr ""
#: paperless_mail/models.py:177
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:184
msgid "assign title from"
msgstr ""
#: paperless_mail/models.py:194
msgid "assign this tag"
msgstr ""
#: paperless_mail/models.py:202
msgid "assign this document type"
msgstr ""
#: paperless_mail/models.py:206
msgid "assign correspondent from"
msgstr ""
#: paperless_mail/models.py:216
msgid "assign this correspondent"
msgstr ""

23
src/paperless/asgi.py Normal file
View File

@@ -0,0 +1,23 @@
import os
from django.core.asgi import get_asgi_application
# Fetch Django ASGI application early to ensure AppRegistry is populated
# before importing consumers and AuthMiddlewareStack that may import ORM
# models.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "paperless.settings")
django_asgi_app = get_asgi_application()
from channels.auth import AuthMiddlewareStack # NOQA: E402
from channels.routing import ProtocolTypeRouter, URLRouter # NOQA: E402
from paperless.urls import websocket_urlpatterns # NOQA: E402
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns
)
),
})

View File

@@ -1,4 +1,5 @@
from django.conf import settings
from django.contrib import auth
from django.contrib.auth.models import User
from django.utils.deprecation import MiddlewareMixin
from rest_framework import authentication
@@ -11,6 +12,7 @@ class AutoLoginMiddleware(MiddlewareMixin):
try:
request.user = User.objects.get(
username=settings.AUTO_LOGIN_USERNAME)
auth.login(request, request.user)
except User.DoesNotExist:
pass
@@ -33,5 +35,4 @@ class HttpRemoteUserMiddleware(RemoteUserMiddleware):
""" This class allows authentication via HTTP_REMOTE_USER which is set for
example by certain SSO applications.
"""
header = 'HTTP_REMOTE_USER'
header = settings.HTTP_REMOTE_USER_HEADER_NAME

View File

@@ -1,5 +1,6 @@
import os
import shutil
import stat
from django.conf import settings
from django.core.checks import Error, Warning, register
@@ -16,16 +17,29 @@ writeable_hint = (
def path_check(var, directory):
messages = []
if directory:
if not os.path.exists(directory):
if not os.path.isdir(directory):
messages.append(Error(
exists_message.format(var),
exists_hint.format(directory)
))
elif not os.access(directory, os.W_OK | os.X_OK):
messages.append(Error(
writeable_message.format(var),
writeable_hint.format(directory)
))
else:
test_file = os.path.join(
directory, f'__paperless_write_test_{os.getpid()}__'
)
try:
with open(test_file, 'w'):
pass
except PermissionError:
messages.append(Error(
writeable_message.format(var),
writeable_hint.format(
f'\n{stat.filemode(os.stat(directory).st_mode)} '
f'{directory}\n')
))
finally:
if os.path.isfile(test_file):
os.remove(test_file)
return messages

View File

@@ -0,0 +1,29 @@
import json
from asgiref.sync import async_to_sync
from channels.exceptions import DenyConnection, AcceptConnection
from channels.generic.websocket import WebsocketConsumer
class StatusConsumer(WebsocketConsumer):
def _authenticated(self):
return 'user' in self.scope and self.scope['user'].is_authenticated
def connect(self):
if not self._authenticated():
raise DenyConnection()
else:
async_to_sync(self.channel_layer.group_add)(
'status_updates', self.channel_name)
raise AcceptConnection()
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
'status_updates', self.channel_name)
def status_update(self, event):
if not self._authenticated():
self.close()
else:
self.send(json.dumps(event['data']))

View File

@@ -0,0 +1,20 @@
from django.conf import settings
from paperless import version
class ApiVersionMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if request.user.is_authenticated:
versions = settings.REST_FRAMEWORK['ALLOWED_VERSIONS']
response['X-Api-Version'] = versions[len(versions)-1]
response['X-Version'] = ".".join(
[str(_) for _ in version.__version__]
)
return response

View File

@@ -4,7 +4,7 @@ import multiprocessing
import os
import re
import dateparser
from concurrent_log_handler.queue import setup_logging_queues
from dotenv import load_dotenv
from django.utils.translation import gettext_lazy as _
@@ -63,6 +63,8 @@ MEDIA_LOCK = os.path.join(MEDIA_ROOT, "media.lock")
INDEX_DIR = os.path.join(DATA_DIR, "index")
MODEL_FILE = os.path.join(DATA_DIR, "classification_model.pickle")
LOGGING_DIR = os.getenv('PAPERLESS_LOGGING_DIR', os.path.join(DATA_DIR, "log"))
CONSUMPTION_DIR = os.getenv("PAPERLESS_CONSUMPTION_DIR", os.path.join(BASE_DIR, "..", "consume"))
# This will be created if it doesn't exist
@@ -102,12 +104,20 @@ INSTALLED_APPS = [
] + env_apps
if DEBUG:
INSTALLED_APPS.append("channels")
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication'
]
],
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
'DEFAULT_VERSION': '1',
# Make sure these are ordered and that the most recent version appears
# last
'ALLOWED_VERSIONS': ['1', '2']
}
if DEBUG:
@@ -123,6 +133,7 @@ MIDDLEWARE = [
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'paperless.middleware.ApiVersionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
@@ -131,12 +142,17 @@ MIDDLEWARE = [
ROOT_URLCONF = 'paperless.urls'
FORCE_SCRIPT_NAME = os.getenv("PAPERLESS_FORCE_SCRIPT_NAME")
BASE_URL = (FORCE_SCRIPT_NAME or "") + "/"
LOGIN_URL = BASE_URL + "accounts/login/"
LOGOUT_REDIRECT_URL = os.getenv("PAPERLESS_LOGOUT_REDIRECT_URL")
WSGI_APPLICATION = 'paperless.wsgi.application'
ASGI_APPLICATION = "paperless.asgi.application"
STATIC_URL = os.getenv("PAPERLESS_STATIC_URL", "/static/")
STATIC_URL = os.getenv("PAPERLESS_STATIC_URL", BASE_URL + "static/")
WHITENOISE_STATIC_PREFIX = "/static/"
# what is this used for?
# TODO: what is this used for?
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
@@ -153,6 +169,17 @@ TEMPLATES = [
},
]
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [os.getenv("PAPERLESS_REDIS", "redis://localhost:6379")],
"capacity": 2000, # default 100
"expiry": 15, # default 60
},
},
}
###############################################################################
# Security #
###############################################################################
@@ -166,6 +193,7 @@ if AUTO_LOGIN_USERNAME:
MIDDLEWARE.insert(_index+1, 'paperless.auth.AutoLoginMiddleware')
ENABLE_HTTP_REMOTE_USER = __get_boolean("PAPERLESS_ENABLE_HTTP_REMOTE_USER")
HTTP_REMOTE_USER_HEADER_NAME = os.getenv("PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME", "HTTP_REMOTE_USER")
if ENABLE_HTTP_REMOTE_USER:
MIDDLEWARE.append(
@@ -264,6 +292,8 @@ if os.getenv("PAPERLESS_DBHOST"):
if os.getenv("PAPERLESS_DBPORT"):
DATABASES["default"]["PORT"] = os.getenv("PAPERLESS_DBPORT")
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
###############################################################################
# Internationalization #
###############################################################################
@@ -271,10 +301,20 @@ if os.getenv("PAPERLESS_DBHOST"):
LANGUAGE_CODE = 'en-us'
LANGUAGES = [
("en-us", _("English")),
("de", _("German")),
("en-us", _("English (US)")),
("en-gb", _("English (GB)")),
("de-de", _("German")),
("nl-nl", _("Dutch")),
("fr", _("French"))
("fr-fr", _("French")),
("pt-br", _("Portuguese (Brazil)")),
("pt-pt", _("Portuguese")),
("it-it", _("Italian")),
("ro-ro", _("Romanian")),
("ru-ru", _("Russian")),
("es-es", _("Spanish")),
("pl-pl", _("Polish")),
("sv-se", _("Swedish")),
("lb-lu", _("Luxembourgish")),
]
LOCALE_PATHS = [
@@ -293,14 +333,19 @@ USE_TZ = True
# Logging #
###############################################################################
DISABLE_DBHANDLER = __get_boolean("PAPERLESS_DISABLE_DBHANDLER")
setup_logging_queues()
os.makedirs(LOGGING_DIR, exist_ok=True)
LOGROTATE_MAX_SIZE = os.getenv("PAPERLESS_LOGROTATE_MAX_SIZE", 1024*1024)
LOGROTATE_MAX_BACKUPS = os.getenv("PAPERLESS_LOGROTATE_MAX_BACKUPS", 20)
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {message}',
'format': '[{asctime}] [{levelname}] [{name}] {message}',
'style': '{',
},
'simple': {
@@ -309,34 +354,39 @@ LOGGING = {
},
},
"handlers": {
"db": {
"level": "DEBUG",
"class": "documents.loggers.PaperlessHandler",
},
"console": {
"level": "DEBUG" if DEBUG else "INFO",
"class": "logging.StreamHandler",
"formatter": "verbose",
},
"file_paperless": {
"class": "concurrent_log_handler.ConcurrentRotatingFileHandler",
"formatter": "verbose",
"filename": os.path.join(LOGGING_DIR, "paperless.log"),
"maxBytes": LOGROTATE_MAX_SIZE,
"backupCount": LOGROTATE_MAX_BACKUPS
},
"file_mail": {
"class": "concurrent_log_handler.ConcurrentRotatingFileHandler",
"formatter": "verbose",
"filename": os.path.join(LOGGING_DIR, "mail.log"),
"maxBytes": LOGROTATE_MAX_SIZE,
"backupCount": LOGROTATE_MAX_BACKUPS
}
},
"root": {
"handlers": ["console"],
"level": "DEBUG",
"handlers": ["console"]
},
"loggers": {
"documents": {
"handlers": ["db"],
"propagate": True,
"paperless": {
"handlers": ["file_paperless"],
"level": "DEBUG"
},
"paperless_mail": {
"handlers": ["db"],
"propagate": True,
},
"paperless_tesseract": {
"handlers": ["db"],
"propagate": True,
},
},
"handlers": ["file_mail"],
"level": "DEBUG"
}
}
}
###############################################################################
@@ -354,8 +404,10 @@ LOGGING = {
def default_task_workers():
# always leave one core open
available_cores = max(multiprocessing.cpu_count() - 1, 1)
available_cores = max(multiprocessing.cpu_count(), 1)
try:
if available_cores < 4:
return available_cores
return max(
math.floor(math.sqrt(available_cores)),
1
@@ -369,6 +421,9 @@ TASK_WORKERS = int(os.getenv("PAPERLESS_TASK_WORKERS", default_task_workers()))
Q_CLUSTER = {
'name': 'paperless',
'catch_up': False,
'recycle': 1,
'retry': 1800,
'timeout': 1800,
'workers': TASK_WORKERS,
'redis': os.getenv("PAPERLESS_REDIS", "redis://localhost:6379")
}
@@ -376,7 +431,7 @@ Q_CLUSTER = {
def default_threads_per_worker(task_workers):
# always leave one core open
available_cores = max(multiprocessing.cpu_count() - 1, 1)
available_cores = max(multiprocessing.cpu_count(), 1)
try:
return max(
math.floor(available_cores / task_workers),
@@ -394,10 +449,22 @@ THREADS_PER_WORKER = os.getenv("PAPERLESS_THREADS_PER_WORKER", default_threads_p
CONSUMER_POLLING = int(os.getenv("PAPERLESS_CONSUMER_POLLING", 0))
CONSUMER_POLLING_DELAY = int(os.getenv("PAPERLESS_CONSUMER_POLLING_DELAY", 5))
CONSUMER_POLLING_RETRY_COUNT = int(
os.getenv("PAPERLESS_CONSUMER_POLLING_RETRY_COUNT", 5)
)
CONSUMER_DELETE_DUPLICATES = __get_boolean("PAPERLESS_CONSUMER_DELETE_DUPLICATES")
CONSUMER_RECURSIVE = __get_boolean("PAPERLESS_CONSUMER_RECURSIVE")
# Ignore glob patterns, relative to PAPERLESS_CONSUMPTION_DIR
CONSUMER_IGNORE_PATTERNS = list(
json.loads(
os.getenv("PAPERLESS_CONSUMER_IGNORE_PATTERNS",
'[".DS_STORE/*", "._*", ".stfolder/*"]')))
CONSUMER_SUBDIRS_AS_TAGS = __get_boolean("PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS")
OPTIMIZE_THUMBNAILS = __get_boolean("PAPERLESS_OPTIMIZE_THUMBNAILS", "true")
@@ -418,6 +485,14 @@ OCR_MODE = os.getenv("PAPERLESS_OCR_MODE", "skip")
OCR_IMAGE_DPI = os.getenv("PAPERLESS_OCR_IMAGE_DPI")
OCR_CLEAN = os.getenv("PAPERLESS_OCR_CLEAN", "clean")
OCR_DESKEW = __get_boolean("PAPERLESS_OCR_DESKEW", "true")
OCR_ROTATE_PAGES = __get_boolean("PAPERLESS_OCR_ROTATE_PAGES", "true")
OCR_ROTATE_PAGES_THRESHOLD = float(os.getenv("PAPERLESS_OCR_ROTATE_PAGES_THRESHOLD", 12.0))
OCR_USER_ARGS = os.getenv("PAPERLESS_OCR_USER_ARGS", "{}")
# GNUPG needs a home directory for some reason
@@ -477,7 +552,11 @@ if PAPERLESS_TIKA_ENABLED:
# List dates that should be ignored when trying to parse date from document text
IGNORE_DATES = set()
for s in os.getenv("PAPERLESS_IGNORE_DATES", "").split(","):
d = dateparser.parse(s)
if d:
IGNORE_DATES.add(d.date())
if os.getenv("PAPERLESS_IGNORE_DATES", ""):
import dateparser
for s in os.getenv("PAPERLESS_IGNORE_DATES", "").split(","):
d = dateparser.parse(s)
if d:
IGNORE_DATES.add(d.date())

View File

@@ -0,0 +1,60 @@
from unittest import mock
from channels.layers import get_channel_layer
from channels.testing import WebsocketCommunicator
from django.test import TestCase, override_settings
from paperless.asgi import application
TEST_CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer',
},
}
class TestWebSockets(TestCase):
@override_settings(CHANNEL_LAYERS=TEST_CHANNEL_LAYERS)
async def test_no_auth(self):
communicator = WebsocketCommunicator(application, "/ws/status/")
connected, subprotocol = await communicator.connect()
self.assertFalse(connected)
await communicator.disconnect()
@override_settings(CHANNEL_LAYERS=TEST_CHANNEL_LAYERS)
@mock.patch("paperless.consumers.StatusConsumer._authenticated")
async def test_auth(self, _authenticated):
_authenticated.return_value = True
communicator = WebsocketCommunicator(application, "/ws/status/")
connected, subprotocol = await communicator.connect()
self.assertTrue(connected)
await communicator.disconnect()
@override_settings(CHANNEL_LAYERS=TEST_CHANNEL_LAYERS)
@mock.patch("paperless.consumers.StatusConsumer._authenticated")
async def test_receive(self, _authenticated):
_authenticated.return_value = True
communicator = WebsocketCommunicator(application, "/ws/status/")
connected, subprotocol = await communicator.connect()
self.assertTrue(connected)
message = {
"task_id": "test"
}
channel_layer = get_channel_layer()
await channel_layer.group_send("status_updates", {
"type": "status_update",
"data": message
})
response = await communicator.receive_json_from()
self.assertEqual(response, message)
await communicator.disconnect()

42
src/paperless/urls.py Executable file → Normal file
View File

@@ -9,28 +9,31 @@ from rest_framework.routers import DefaultRouter
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from paperless.consumers import StatusConsumer
from documents.views import (
CorrespondentViewSet,
DocumentViewSet,
UnifiedSearchViewSet,
LogViewSet,
TagViewSet,
DocumentTypeViewSet,
SearchView,
IndexView,
SearchAutoCompleteView,
StatisticsView,
PostDocumentView,
SavedViewViewSet,
BulkEditView,
SelectionDataView
SelectionDataView,
BulkDownloadView
)
from paperless.views import FaviconView
api_router = DefaultRouter()
api_router.register(r"correspondents", CorrespondentViewSet)
api_router.register(r"document_types", DocumentTypeViewSet)
api_router.register(r"documents", DocumentViewSet)
api_router.register(r"logs", LogViewSet)
api_router.register(r"documents", UnifiedSearchViewSet)
api_router.register(r"logs", LogViewSet, basename="logs")
api_router.register(r"tags", TagViewSet)
api_router.register(r"saved_views", SavedViewViewSet)
@@ -45,10 +48,6 @@ urlpatterns = [
SearchAutoCompleteView.as_view(),
name="autocomplete"),
re_path(r"^search/",
SearchView.as_view(),
name="search"),
re_path(r"^statistics/",
StatisticsView.as_view(),
name="statistics"),
@@ -62,6 +61,9 @@ urlpatterns = [
re_path(r"^documents/selection_data/", SelectionDataView.as_view(),
name="selection_data"),
re_path(r"^documents/bulk_download/", BulkDownloadView.as_view(),
name="bulk_download"),
path('token/', views.obtain_auth_token)
] + api_router.urls)),
@@ -73,31 +75,41 @@ urlpatterns = [
re_path(r"^fetch/", include([
re_path(
r"^doc/(?P<pk>\d+)$",
RedirectView.as_view(url='/api/documents/%(pk)s/download/'),
RedirectView.as_view(url=settings.BASE_URL +
'api/documents/%(pk)s/download/'),
),
re_path(
r"^thumb/(?P<pk>\d+)$",
RedirectView.as_view(url='/api/documents/%(pk)s/thumb/'),
RedirectView.as_view(url=settings.BASE_URL +
'api/documents/%(pk)s/thumb/'),
),
re_path(
r"^preview/(?P<pk>\d+)$",
RedirectView.as_view(url='/api/documents/%(pk)s/preview/'),
RedirectView.as_view(url=settings.BASE_URL +
'api/documents/%(pk)s/preview/'),
),
])),
re_path(r"^push$", csrf_exempt(
RedirectView.as_view(url='/api/documents/post_document/'))),
RedirectView.as_view(url=settings.BASE_URL +
'api/documents/post_document/'))),
# Frontend assets TODO: this is pretty bad, but it works.
path('assets/<path:path>',
RedirectView.as_view(url='/static/frontend/en-US/assets/%(path)s')),
RedirectView.as_view(url=settings.STATIC_URL +
'frontend/en-US/assets/%(path)s')),
# TODO: with localization, this is even worse! :/
# login, logout
path('accounts/', include('django.contrib.auth.urls')),
# Root of the Frontent
re_path(r".*", login_required(IndexView.as_view())),
re_path(r".*", login_required(IndexView.as_view()), name='base'),
]
websocket_urlpatterns = [
re_path(r'ws/status/$', StatusConsumer.as_asgi()),
]
# Text in each page's <h1> (and above login form).

View File

@@ -1 +1 @@
__version__ = (1, 0, 0)
__version__ = (1, 5, 0)

11
src/paperless/workers.py Normal file
View File

@@ -0,0 +1,11 @@
import os
from uvicorn.workers import UvicornWorker
from django.conf import settings
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "paperless.settings")
class ConfigurableWorker(UvicornWorker):
CONFIG_KWARGS = {
"root_path": settings.FORCE_SCRIPT_NAME or "",
}

View File

@@ -8,6 +8,18 @@ class MailAccountAdmin(admin.ModelAdmin):
list_display = ("name", "imap_server", "username")
fieldsets = [
(None, {
'fields': ['name', 'imap_server', 'imap_port']
}),
(_("Authentication"), {
'fields': ['imap_security', 'username', 'password']
}),
(_("Advanced settings"), {
'fields': ['character_set']
})
]
class MailRuleAdmin(admin.ModelAdmin):

Some files were not shown because too many files have changed in this diff Show More