mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Feature: document history (audit log UI) (#6388)
This commit is contained in:
@@ -882,7 +882,12 @@ class CustomFieldInstance(models.Model):
|
||||
|
||||
|
||||
if settings.AUDIT_LOG_ENABLED:
|
||||
auditlog.register(Document, m2m_fields={"tags"})
|
||||
auditlog.register(
|
||||
Document,
|
||||
m2m_fields={"tags"},
|
||||
mask_fields=["content"],
|
||||
exclude_fields=["modified"],
|
||||
)
|
||||
auditlog.register(Correspondent)
|
||||
auditlog.register(Tag)
|
||||
auditlog.register(DocumentType)
|
||||
|
@@ -5,6 +5,7 @@ import zoneinfo
|
||||
from decimal import Decimal
|
||||
|
||||
import magic
|
||||
from auditlog.context import set_actor
|
||||
from celery import states
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group
|
||||
@@ -746,7 +747,11 @@ class DocumentSerializer(
|
||||
for tag in instance.tags.all()
|
||||
if tag not in inbox_tags_not_being_added
|
||||
]
|
||||
super().update(instance, validated_data)
|
||||
if settings.AUDIT_LOG_ENABLED:
|
||||
with set_actor(self.user):
|
||||
super().update(instance, validated_data)
|
||||
else:
|
||||
super().update(instance, validated_data)
|
||||
return instance
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@@ -316,6 +316,133 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
response = self.client.get(f"/api/documents/{doc.pk}/thumb/")
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_document_history_action(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document
|
||||
WHEN:
|
||||
- Document is updated
|
||||
THEN:
|
||||
- Audit log contains changes
|
||||
"""
|
||||
doc = Document.objects.create(
|
||||
title="First title",
|
||||
checksum="123",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
self.client.force_login(user=self.user)
|
||||
self.client.patch(
|
||||
f"/api/documents/{doc.pk}/",
|
||||
{"title": "New title"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
response = self.client.get(f"/api/documents/{doc.pk}/history/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
self.assertEqual(response.data[0]["actor"]["id"], self.user.id)
|
||||
self.assertEqual(response.data[0]["action"], "update")
|
||||
self.assertEqual(
|
||||
response.data[0]["changes"],
|
||||
{"title": ["First title", "New title"]},
|
||||
)
|
||||
|
||||
def test_document_history_action_w_custom_fields(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document with custom fields
|
||||
WHEN:
|
||||
- Document is updated
|
||||
THEN:
|
||||
- Audit log contains custom field changes
|
||||
"""
|
||||
doc = Document.objects.create(
|
||||
title="First title",
|
||||
checksum="123",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
custom_field = CustomField.objects.create(
|
||||
name="custom field str",
|
||||
data_type=CustomField.FieldDataType.STRING,
|
||||
)
|
||||
self.client.force_login(user=self.user)
|
||||
self.client.patch(
|
||||
f"/api/documents/{doc.pk}/",
|
||||
data={
|
||||
"custom_fields": [
|
||||
{
|
||||
"field": custom_field.pk,
|
||||
"value": "custom value",
|
||||
},
|
||||
],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
response = self.client.get(f"/api/documents/{doc.pk}/history/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data[1]["actor"]["id"], self.user.id)
|
||||
self.assertEqual(response.data[1]["action"], "create")
|
||||
self.assertEqual(
|
||||
response.data[1]["changes"],
|
||||
{
|
||||
"custom_fields": {
|
||||
"type": "custom_field",
|
||||
"field": "custom field str",
|
||||
"value": "custom value",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@override_settings(AUDIT_LOG_ENABLED=False)
|
||||
def test_document_history_action_disabled(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Audit log is disabled
|
||||
WHEN:
|
||||
- Document is updated
|
||||
- Audit log is requested
|
||||
THEN:
|
||||
- Audit log returns HTTP 400 Bad Request
|
||||
"""
|
||||
doc = Document.objects.create(
|
||||
title="First title",
|
||||
checksum="123",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
self.client.force_login(user=self.user)
|
||||
self.client.patch(
|
||||
f"/api/documents/{doc.pk}/",
|
||||
{"title": "New title"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
response = self.client.get(f"/api/documents/{doc.pk}/history/")
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_document_history_insufficient_perms(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Audit log is disabled
|
||||
WHEN:
|
||||
- Document is updated
|
||||
- Audit log is requested
|
||||
THEN:
|
||||
- Audit log returns HTTP 400 Bad Request
|
||||
"""
|
||||
user = User.objects.create_user(username="test")
|
||||
user.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
||||
self.client.force_login(user=user)
|
||||
doc = Document.objects.create(
|
||||
title="First title",
|
||||
checksum="123",
|
||||
mime_type="application/pdf",
|
||||
owner=user,
|
||||
)
|
||||
|
||||
response = self.client.get(f"/api/documents/{doc.pk}/history/")
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_document_filters(self):
|
||||
doc1 = Document.objects.create(
|
||||
title="none1",
|
||||
|
@@ -39,6 +39,7 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
|
||||
{
|
||||
"app_title": None,
|
||||
"app_logo": None,
|
||||
"auditlog_enabled": True,
|
||||
"update_checking": {
|
||||
"backend_setting": "default",
|
||||
},
|
||||
|
@@ -4,6 +4,7 @@ import tempfile
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from auditlog.context import disable_auditlog
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import DatabaseError
|
||||
@@ -143,7 +144,9 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
# Set a correspondent and save the document
|
||||
document.correspondent = Correspondent.objects.get_or_create(name="test")[0]
|
||||
|
||||
with mock.patch("documents.signals.handlers.Document.objects.filter") as m:
|
||||
with mock.patch(
|
||||
"documents.signals.handlers.Document.objects.filter",
|
||||
) as m, disable_auditlog():
|
||||
m.side_effect = DatabaseError()
|
||||
document.save()
|
||||
|
||||
@@ -557,20 +560,21 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
@override_settings(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()
|
||||
with disable_auditlog():
|
||||
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()
|
||||
doc.save()
|
||||
|
||||
m.assert_not_called()
|
||||
m.assert_not_called()
|
||||
|
||||
|
||||
class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
|
@@ -18,6 +18,7 @@ import pathvalidate
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import connections
|
||||
from django.db.migrations.loader import MigrationLoader
|
||||
from django.db.migrations.recorder import MigrationRecorder
|
||||
@@ -105,6 +106,7 @@ from documents.matching import match_storage_paths
|
||||
from documents.matching import match_tags
|
||||
from documents.models import Correspondent
|
||||
from documents.models import CustomField
|
||||
from documents.models import CustomFieldInstance
|
||||
from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import Note
|
||||
@@ -729,6 +731,66 @@ class DocumentViewSet(
|
||||
]
|
||||
return Response(links)
|
||||
|
||||
@action(methods=["get"], detail=True, name="Audit Trail")
|
||||
def history(self, request, pk=None):
|
||||
if not settings.AUDIT_LOG_ENABLED:
|
||||
return HttpResponseBadRequest("Audit log is disabled")
|
||||
try:
|
||||
doc = Document.objects.get(pk=pk)
|
||||
if not request.user.has_perm("auditlog.view_logentry") or (
|
||||
doc.owner is not None and doc.owner != request.user
|
||||
):
|
||||
return HttpResponseForbidden(
|
||||
"Insufficient permissions",
|
||||
)
|
||||
except Document.DoesNotExist: # pragma: no cover
|
||||
raise Http404
|
||||
|
||||
# documents
|
||||
entries = [
|
||||
{
|
||||
"id": entry.id,
|
||||
"timestamp": entry.timestamp,
|
||||
"action": entry.get_action_display(),
|
||||
"changes": entry.changes,
|
||||
"actor": (
|
||||
{"id": entry.actor.id, "username": entry.actor.username}
|
||||
if entry.actor
|
||||
else None
|
||||
),
|
||||
}
|
||||
for entry in LogEntry.objects.filter(object_pk=doc.pk).select_related(
|
||||
"actor",
|
||||
)
|
||||
]
|
||||
|
||||
# custom fields
|
||||
for entry in LogEntry.objects.filter(
|
||||
object_pk__in=doc.custom_fields.values_list("id", flat=True),
|
||||
content_type=ContentType.objects.get_for_model(CustomFieldInstance),
|
||||
).select_related("actor"):
|
||||
entries.append(
|
||||
{
|
||||
"id": entry.id,
|
||||
"timestamp": entry.timestamp,
|
||||
"action": entry.get_action_display(),
|
||||
"changes": {
|
||||
"custom_fields": {
|
||||
"type": "custom_field",
|
||||
"field": str(entry.object_repr).split(":")[0].strip(),
|
||||
"value": str(entry.object_repr).split(":")[1].strip(),
|
||||
},
|
||||
},
|
||||
"actor": (
|
||||
{"id": entry.actor.id, "username": entry.actor.username}
|
||||
if entry.actor
|
||||
else None
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
return Response(sorted(entries, key=lambda x: x["timestamp"], reverse=True))
|
||||
|
||||
|
||||
class SearchResultSerializer(DocumentSerializer, PassUserMixin):
|
||||
def to_representation(self, instance):
|
||||
@@ -1267,6 +1329,8 @@ class UiSettingsView(GenericAPIView):
|
||||
if general_config.app_logo is not None and len(general_config.app_logo) > 0:
|
||||
ui_settings["app_logo"] = general_config.app_logo
|
||||
|
||||
ui_settings["auditlog_enabled"] = settings.AUDIT_LOG_ENABLED
|
||||
|
||||
user_resp = {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
|
@@ -2,7 +2,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-04-19 01:13-0700\n"
|
||||
"POT-Creation-Date: 2024-04-19 01:15-0700\n"
|
||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
@@ -25,27 +25,27 @@ msgstr ""
|
||||
msgid "owner"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:53 documents/models.py:897
|
||||
#: documents/models.py:53 documents/models.py:902
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:54 documents/models.py:898
|
||||
#: documents/models.py:54 documents/models.py:903
|
||||
msgid "Any word"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:55 documents/models.py:899
|
||||
#: documents/models.py:55 documents/models.py:904
|
||||
msgid "All words"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:56 documents/models.py:900
|
||||
#: documents/models.py:56 documents/models.py:905
|
||||
msgid "Exact match"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:57 documents/models.py:901
|
||||
#: documents/models.py:57 documents/models.py:906
|
||||
msgid "Regular expression"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:58 documents/models.py:902
|
||||
#: documents/models.py:58 documents/models.py:907
|
||||
msgid "Fuzzy word"
|
||||
msgstr ""
|
||||
|
||||
@@ -53,20 +53,20 @@ msgstr ""
|
||||
msgid "Automatic"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:62 documents/models.py:397 documents/models.py:1218
|
||||
#: documents/models.py:62 documents/models.py:397 documents/models.py:1223
|
||||
#: paperless_mail/models.py:18 paperless_mail/models.py:93
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:64 documents/models.py:958
|
||||
#: documents/models.py:64 documents/models.py:963
|
||||
msgid "match"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:67 documents/models.py:961
|
||||
#: documents/models.py:67 documents/models.py:966
|
||||
msgid "matching algorithm"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:72 documents/models.py:966
|
||||
#: documents/models.py:72 documents/models.py:971
|
||||
msgid "is insensitive"
|
||||
msgstr ""
|
||||
|
||||
@@ -615,246 +615,246 @@ msgstr ""
|
||||
msgid "custom field instances"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:905
|
||||
#: documents/models.py:910
|
||||
msgid "Consumption Started"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:906
|
||||
#: documents/models.py:911
|
||||
msgid "Document Added"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:907
|
||||
#: documents/models.py:912
|
||||
msgid "Document Updated"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:910
|
||||
#: documents/models.py:915
|
||||
msgid "Consume Folder"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:911
|
||||
#: documents/models.py:916
|
||||
msgid "Api Upload"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:912
|
||||
#: documents/models.py:917
|
||||
msgid "Mail Fetch"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:915
|
||||
#: documents/models.py:920
|
||||
msgid "Workflow Trigger Type"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:927
|
||||
#: documents/models.py:932
|
||||
msgid "filter path"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:932
|
||||
#: documents/models.py:937
|
||||
msgid ""
|
||||
"Only consume documents with a path that matches this if specified. Wildcards "
|
||||
"specified as * are allowed. Case insensitive."
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:939
|
||||
#: documents/models.py:944
|
||||
msgid "filter filename"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:944 paperless_mail/models.py:148
|
||||
#: documents/models.py:949 paperless_mail/models.py:148
|
||||
msgid ""
|
||||
"Only consume documents which entirely match this filename if specified. "
|
||||
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:955
|
||||
#: documents/models.py:960
|
||||
msgid "filter documents from this mail rule"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:971
|
||||
#: documents/models.py:976
|
||||
msgid "has these tag(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:979
|
||||
#: documents/models.py:984
|
||||
msgid "has this document type"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:987
|
||||
#: documents/models.py:992
|
||||
msgid "has this correspondent"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:991
|
||||
#: documents/models.py:996
|
||||
msgid "workflow trigger"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:992
|
||||
#: documents/models.py:997
|
||||
msgid "workflow triggers"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1002
|
||||
#: documents/models.py:1007
|
||||
msgid "Assignment"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1006
|
||||
#: documents/models.py:1011
|
||||
msgid "Removal"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1010
|
||||
#: documents/models.py:1015
|
||||
msgid "Workflow Action Type"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1016
|
||||
#: documents/models.py:1021
|
||||
msgid "assign title"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1021
|
||||
#: documents/models.py:1026
|
||||
msgid ""
|
||||
"Assign a document title, can include some placeholders, see documentation."
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1030 paperless_mail/models.py:216
|
||||
#: documents/models.py:1035 paperless_mail/models.py:216
|
||||
msgid "assign this tag"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1039 paperless_mail/models.py:224
|
||||
#: documents/models.py:1044 paperless_mail/models.py:224
|
||||
msgid "assign this document type"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1048 paperless_mail/models.py:238
|
||||
#: documents/models.py:1053 paperless_mail/models.py:238
|
||||
msgid "assign this correspondent"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1057
|
||||
#: documents/models.py:1062
|
||||
msgid "assign this storage path"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1066
|
||||
#: documents/models.py:1071
|
||||
msgid "assign this owner"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1073
|
||||
#: documents/models.py:1078
|
||||
msgid "grant view permissions to these users"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1080
|
||||
#: documents/models.py:1085
|
||||
msgid "grant view permissions to these groups"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1087
|
||||
#: documents/models.py:1092
|
||||
msgid "grant change permissions to these users"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1094
|
||||
#: documents/models.py:1099
|
||||
msgid "grant change permissions to these groups"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1101
|
||||
#: documents/models.py:1106
|
||||
msgid "assign these custom fields"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1108
|
||||
#: documents/models.py:1113
|
||||
msgid "remove these tag(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1113
|
||||
#: documents/models.py:1118
|
||||
msgid "remove all tags"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1120
|
||||
#: documents/models.py:1125
|
||||
msgid "remove these document type(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1125
|
||||
#: documents/models.py:1130
|
||||
msgid "remove all document types"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1132
|
||||
#: documents/models.py:1137
|
||||
msgid "remove these correspondent(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1137
|
||||
#: documents/models.py:1142
|
||||
msgid "remove all correspondents"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1144
|
||||
#: documents/models.py:1149
|
||||
msgid "remove these storage path(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1149
|
||||
#: documents/models.py:1154
|
||||
msgid "remove all storage paths"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1156
|
||||
#: documents/models.py:1161
|
||||
msgid "remove these owner(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1161
|
||||
#: documents/models.py:1166
|
||||
msgid "remove all owners"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1168
|
||||
#: documents/models.py:1173
|
||||
msgid "remove view permissions for these users"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1175
|
||||
#: documents/models.py:1180
|
||||
msgid "remove view permissions for these groups"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1182
|
||||
#: documents/models.py:1187
|
||||
msgid "remove change permissions for these users"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1189
|
||||
#: documents/models.py:1194
|
||||
msgid "remove change permissions for these groups"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1194
|
||||
#: documents/models.py:1199
|
||||
msgid "remove all permissions"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1201
|
||||
#: documents/models.py:1206
|
||||
msgid "remove these custom fields"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1206
|
||||
#: documents/models.py:1211
|
||||
msgid "remove all custom fields"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1210
|
||||
#: documents/models.py:1215
|
||||
msgid "workflow action"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1211
|
||||
#: documents/models.py:1216
|
||||
msgid "workflow actions"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1220 paperless_mail/models.py:95
|
||||
#: documents/models.py:1225 paperless_mail/models.py:95
|
||||
msgid "order"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1226
|
||||
#: documents/models.py:1231
|
||||
msgid "triggers"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1233
|
||||
#: documents/models.py:1238
|
||||
msgid "actions"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1236
|
||||
#: documents/models.py:1241
|
||||
msgid "enabled"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:114
|
||||
#: documents/serialisers.py:115
|
||||
#, python-format
|
||||
msgid "Invalid regular expression: %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:417
|
||||
#: documents/serialisers.py:418
|
||||
msgid "Invalid color."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:1143
|
||||
#: documents/serialisers.py:1148
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:1252
|
||||
#: documents/serialisers.py:1257
|
||||
msgid "Invalid variable detected."
|
||||
msgstr ""
|
||||
|
||||
|
Reference in New Issue
Block a user