Squashed commit of the following:

commit 8a0a49dd57
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 23:02:47 2023 -0800

    Fix formatting

commit 66b2d90c50
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 22:36:35 2023 -0800

    Refactor frontend data models

commit 5723bd8dd8
Author: Adam Bogdał <adam@bogdal.pl>
Date:   Wed Dec 20 01:17:43 2023 +0100

    Fix: speed up admin panel for installs with a large number of documents (#5052)

commit 9b08ce1761
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 15:18:51 2023 -0800

    Update PULL_REQUEST_TEMPLATE.md

commit a6248bec2d
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 15:02:05 2023 -0800

    Chore: Update Angular to v17 (#4980)

commit b1f6f52486
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 13:53:56 2023 -0800

    Fix: Dont allow null custom_fields property via API (#5063)

commit 638d9970fd
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 13:43:50 2023 -0800

    Enhancement: symmetric document links (#4907)

commit 5e8de4c1da
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 12:45:04 2023 -0800

    Enhancement: shared icon & shared by me filter (#4859)

commit 088bad9030
Author: Trenton H <797416+stumpylog@users.noreply.github.com>
Date:   Tue Dec 19 12:04:03 2023 -0800

    Bulk updates all the backend libraries (#5061)
This commit is contained in:
shamoon
2023-12-19 23:03:10 -08:00
parent 74e845974c
commit 3e1a3aef4c
209 changed files with 9125 additions and 7680 deletions

View File

@@ -28,8 +28,9 @@ class CorrespondentAdmin(GuardedModelAdmin):
class TagAdmin(GuardedModelAdmin):
list_display = ("name", "color", "match", "matching_algorithm")
list_filter = ("color", "matching_algorithm")
list_filter = ("matching_algorithm",)
list_editable = ("color", "match", "matching_algorithm")
search_fields = ("color", "name")
class DocumentTypeAdmin(GuardedModelAdmin):
@@ -107,6 +108,9 @@ class SavedViewAdmin(GuardedModelAdmin):
inlines = [RuleInline]
def get_queryset(self, request): # pragma: no cover
return super().get_queryset(request).select_related("owner")
class StoragePathInline(admin.TabularInline):
model = StoragePath
@@ -120,8 +124,8 @@ class StoragePathAdmin(GuardedModelAdmin):
class TaskAdmin(admin.ModelAdmin):
list_display = ("task_id", "task_file_name", "task_name", "date_done", "status")
list_filter = ("status", "date_done", "task_file_name", "task_name")
search_fields = ("task_name", "task_id", "status")
list_filter = ("status", "date_done", "task_name")
search_fields = ("task_name", "task_id", "status", "task_file_name")
readonly_fields = (
"task_id",
"task_file_name",
@@ -138,12 +142,25 @@ class NotesAdmin(GuardedModelAdmin):
list_display = ("user", "created", "note", "document")
list_filter = ("created", "user")
list_display_links = ("created",)
raw_id_fields = ("document",)
search_fields = ("document__title",)
def get_queryset(self, request): # pragma: no cover
return (
super()
.get_queryset(request)
.select_related("user", "document__correspondent")
)
class ShareLinksAdmin(GuardedModelAdmin):
list_display = ("created", "expiration", "document")
list_filter = ("created", "expiration", "owner")
list_display_links = ("created",)
raw_id_fields = ("document",)
def get_queryset(self, request): # pragma: no cover
return super().get_queryset(request).select_related("document__correspondent")
class CustomFieldsAdmin(GuardedModelAdmin):
@@ -157,7 +174,15 @@ class CustomFieldInstancesAdmin(GuardedModelAdmin):
fields = ("field", "document", "created", "value")
readonly_fields = ("field", "document", "created", "value")
list_display = ("field", "document", "value", "created")
list_filter = ("document", "created")
search_fields = ("document__title",)
list_filter = ("created", "field")
def get_queryset(self, request): # pragma: no cover
return (
super()
.get_queryset(request)
.select_related("field", "document__correspondent")
)
admin.site.register(Correspondent, CorrespondentAdmin)

View File

@@ -1,7 +1,12 @@
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count
from django.db.models import OuterRef
from django.db.models import Q
from django_filters.rest_framework import BooleanFilter
from django_filters.rest_framework import Filter
from django_filters.rest_framework import FilterSet
from guardian.utils import get_group_obj_perms_model
from guardian.utils import get_user_obj_perms_model
from rest_framework_guardian.filters import ObjectPermissionsFilter
from documents.models import Correspondent
@@ -101,6 +106,39 @@ class TitleContentFilter(Filter):
return qs
class SharedByUser(Filter):
def filter(self, qs, value):
ctype = ContentType.objects.get_for_model(self.model)
UserObjectPermission = get_user_obj_perms_model()
GroupObjectPermission = get_group_obj_perms_model()
return (
qs.filter(
owner_id=value,
)
.annotate(
num_shared_users=Count(
UserObjectPermission.objects.filter(
content_type=ctype,
object_pk=OuterRef("pk"),
).values("user_id"),
),
)
.annotate(
num_shared_groups=Count(
GroupObjectPermission.objects.filter(
content_type=ctype,
object_pk=OuterRef("pk"),
).values("group_id"),
),
)
.filter(
Q(num_shared_users__gt=0) | Q(num_shared_groups__gt=0),
)
if value is not None
else qs
)
class CustomFieldsFilter(Filter):
def filter(self, qs, value):
if value:
@@ -144,6 +182,8 @@ class DocumentFilterSet(FilterSet):
custom_fields__icontains = CustomFieldsFilter()
shared_by__id = SharedByUser()
class Meta:
model = Document
fields = {

View File

@@ -75,6 +75,7 @@ def get_schema():
viewer_id=KEYWORD(commas=True),
checksum=TEXT(),
original_filename=TEXT(sortable=True),
is_shared=BOOLEAN(),
)
@@ -167,6 +168,7 @@ def update_document(writer: AsyncWriter, doc: Document):
viewer_id=viewer_ids if viewer_ids else None,
checksum=doc.checksum,
original_filename=doc.original_filename,
is_shared=len(viewer_ids) > 0,
)
@@ -194,6 +196,7 @@ class DelayedQuery:
"document_type": ("type", ["id", "id__in", "id__none", "isnull"]),
"storage_path": ("path", ["id", "id__in", "id__none", "isnull"]),
"owner": ("owner", ["id", "id__in", "id__none", "isnull"]),
"shared_by": ("shared_by", ["id"]),
"tags": ("tag", ["id__all", "id__in", "id__none"]),
"added": ("added", ["date__lt", "date__gt"]),
"created": ("created", ["date__lt", "date__gt"]),
@@ -233,7 +236,11 @@ class DelayedQuery:
continue
if query_filter == "id":
criterias.append(query.Term(f"{field}_id", value))
if param == "shared_by":
criterias.append(query.Term("is_shared", True))
criterias.append(query.Term("owner_id", value))
else:
criterias.append(query.Term(f"{field}_id", value))
elif query_filter == "id__in":
in_filter = []
for object_id in value.split(","):

View File

@@ -0,0 +1,60 @@
# Generated by Django 4.2.7 on 2023-12-09 18:13
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("documents", "1042_consumptiontemplate_assign_custom_fields_and_more"),
]
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"),
(20, "fulltext query"),
(21, "more like this"),
(22, "has tags in"),
(23, "ASN greater than"),
(24, "ASN less than"),
(25, "storage path is"),
(26, "has correspondent in"),
(27, "does not have correspondent in"),
(28, "has document type in"),
(29, "does not have document type in"),
(30, "has storage path in"),
(31, "does not have storage path in"),
(32, "owner is"),
(33, "has owner in"),
(34, "does not have owner"),
(35, "does not have owner in"),
(36, "has custom field value"),
(37, "is shared by me"),
],
verbose_name="rule type",
),
),
]

View File

@@ -455,6 +455,8 @@ class SavedViewFilterRule(models.Model):
(33, _("has owner in")),
(34, _("does not have owner")),
(35, _("does not have owner in")),
(36, _("has custom field value")),
(37, _("is shared by me")),
]
saved_view = models.ForeignKey(

View File

@@ -8,6 +8,7 @@ from celery import states
from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.validators import URLValidator
from django.utils.crypto import get_random_string
from django.utils.text import slugify
@@ -15,6 +16,8 @@ from django.utils.translation import gettext as _
from drf_writable_nested.serializers import NestedUpdateMixin
from guardian.core import ObjectPermissionChecker
from guardian.shortcuts import get_users_with_perms
from guardian.utils import get_group_obj_perms_model
from guardian.utils import get_user_obj_perms_model
from rest_framework import fields
from rest_framework import serializers
from rest_framework.fields import SerializerMethodField
@@ -160,6 +163,7 @@ class OwnedObjectSerializer(serializers.ModelSerializer, SetPermissionsMixin):
try:
if full_perms:
self.fields.pop("user_can_change")
self.fields.pop("is_shared_by_requester")
else:
self.fields.pop("permissions")
except KeyError:
@@ -205,8 +209,26 @@ class OwnedObjectSerializer(serializers.ModelSerializer, SetPermissionsMixin):
)
)
def get_is_shared_by_requester(self, obj: Document):
ctype = ContentType.objects.get_for_model(obj)
UserObjectPermission = get_user_obj_perms_model()
GroupObjectPermission = get_group_obj_perms_model()
return obj.owner == self.user and (
UserObjectPermission.objects.filter(
content_type=ctype,
object_pk=obj.pk,
).count()
> 0
or GroupObjectPermission.objects.filter(
content_type=ctype,
object_pk=obj.pk,
).count()
> 0
)
permissions = SerializerMethodField(read_only=True)
user_can_change = SerializerMethodField(read_only=True)
is_shared_by_requester = SerializerMethodField(read_only=True)
set_permissions = serializers.DictField(
label="Set permissions",
@@ -449,6 +471,10 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
# This key must exist, as it is validated
data_store_name = type_to_data_store_name_map[custom_field.data_type]
if custom_field.data_type == CustomField.FieldDataType.DOCUMENTLINK:
# prior to update so we can look for any docs that are going to be removed
self.reflect_doclinks(document, custom_field, validated_data["value"])
# Actually update or create the instance, providing the value
# to fill in the correct attribute based on the type
instance, _ = CustomFieldInstance.objects.update_or_create(
@@ -472,6 +498,77 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
URLValidator()(data["value"])
return data
def reflect_doclinks(
self,
document: Document,
field: CustomField,
target_doc_ids: list[int],
):
"""
Add or remove 'symmetrical' links to `document` on all `target_doc_ids`
"""
# Check if any documents are going to be removed from the current list of links and remove the symmetrical links
current_field_instance = CustomFieldInstance.objects.filter(
field=field,
document=document,
).first()
if current_field_instance is not None:
for doc_id in current_field_instance.value:
if doc_id not in target_doc_ids:
self.remove_doclink(document, field, doc_id)
# Create an instance if target doc doesnt have this field or append it to an existing one
existing_custom_field_instances = {
custom_field.document_id: custom_field
for custom_field in CustomFieldInstance.objects.filter(
field=field,
document_id__in=target_doc_ids,
)
}
custom_field_instances_to_create = []
custom_field_instances_to_update = []
for target_doc_id in target_doc_ids:
target_doc_field_instance = existing_custom_field_instances.get(
target_doc_id,
)
if target_doc_field_instance is None:
custom_field_instances_to_create.append(
CustomFieldInstance(
document_id=target_doc_id,
field=field,
value_document_ids=[document.id],
),
)
elif document.id not in target_doc_field_instance.value:
target_doc_field_instance.value_document_ids.append(document.id)
custom_field_instances_to_update.append(target_doc_field_instance)
CustomFieldInstance.objects.bulk_create(custom_field_instances_to_create)
CustomFieldInstance.objects.bulk_update(
custom_field_instances_to_update,
["value_document_ids"],
)
@staticmethod
def remove_doclink(
document: Document,
field: CustomField,
target_doc_id: int,
):
"""
Removes a 'symmetrical' link to `document` from the target document's existing custom field instance
"""
target_doc_field_instance = CustomFieldInstance.objects.filter(
document_id=target_doc_id,
field=field,
).first()
if (
target_doc_field_instance is not None
and document.id in target_doc_field_instance.value
):
target_doc_field_instance.value.remove(document.id)
target_doc_field_instance.save()
class Meta:
model = CustomFieldInstance
fields = [
@@ -494,7 +591,11 @@ class DocumentSerializer(
archived_file_name = SerializerMethodField()
created_date = serializers.DateField(required=False)
custom_fields = CustomFieldInstanceSerializer(many=True, allow_null=True)
custom_fields = CustomFieldInstanceSerializer(
many=True,
allow_null=False,
required=False,
)
owner = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(),
@@ -527,6 +628,21 @@ class DocumentSerializer(
instance.save()
if "created_date" in validated_data:
validated_data.pop("created_date")
if instance.custom_fields.count() > 0 and "custom_fields" in validated_data:
incoming_custom_fields = [
field["field"] for field in validated_data["custom_fields"]
]
for custom_field_instance in instance.custom_fields.filter(
field__data_type=CustomField.FieldDataType.DOCUMENTLINK,
):
if custom_field_instance.field not in incoming_custom_fields:
# Doc link field is being removed entirely
for doc_id in custom_field_instance.value:
CustomFieldInstanceSerializer.remove_doclink(
instance,
custom_field_instance.field,
doc_id,
)
super().update(instance, validated_data)
return instance
@@ -556,6 +672,7 @@ class DocumentSerializer(
"owner",
"permissions",
"user_can_change",
"is_shared_by_requester",
"set_permissions",
"notes",
"custom_fields",

View File

@@ -70,6 +70,12 @@ class TestCustomField(DirectoriesMixin, APITestCase):
checksum="123",
mime_type="application/pdf",
)
doc2 = Document.objects.create(
title="WOW2",
content="the content2",
checksum="1234",
mime_type="application/pdf",
)
custom_field_string = CustomField.objects.create(
name="Test Custom Field String",
data_type=CustomField.FieldDataType.STRING,
@@ -139,7 +145,7 @@ class TestCustomField(DirectoriesMixin, APITestCase):
},
{
"field": custom_field_documentlink.id,
"value": [1, 2, 3],
"value": [doc2.id],
},
],
},
@@ -160,7 +166,7 @@ class TestCustomField(DirectoriesMixin, APITestCase):
{"field": custom_field_url.id, "value": "https://example.com"},
{"field": custom_field_float.id, "value": 12.3456},
{"field": custom_field_monetary.id, "value": 11.10},
{"field": custom_field_documentlink.id, "value": [1, 2, 3]},
{"field": custom_field_documentlink.id, "value": [doc2.id]},
],
)
@@ -393,3 +399,137 @@ class TestCustomField(DirectoriesMixin, APITestCase):
self.assertEqual(CustomFieldInstance.objects.count(), 0)
self.assertEqual(len(doc.custom_fields.all()), 0)
def test_custom_field_not_null(self):
"""
GIVEN:
- Existing document
WHEN:
- API request with custom_fields set to null
THEN:
- HTTP 400 is returned
"""
doc = Document.objects.create(
title="WOW",
content="the content",
checksum="123",
mime_type="application/pdf",
)
resp = self.client.patch(
f"/api/documents/{doc.id}/",
data={
"custom_fields": None,
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
def test_bidirectional_doclink_fields(self):
"""
GIVEN:
- Existing document
WHEN:
- Doc links are added or removed
THEN:
- Symmetrical link is created or removed as expected
"""
doc1 = Document.objects.create(
title="WOW1",
content="1",
checksum="1",
mime_type="application/pdf",
)
doc2 = Document.objects.create(
title="WOW2",
content="the content2",
checksum="2",
mime_type="application/pdf",
)
doc3 = Document.objects.create(
title="WOW3",
content="the content3",
checksum="3",
mime_type="application/pdf",
)
doc4 = Document.objects.create(
title="WOW4",
content="the content4",
checksum="4",
mime_type="application/pdf",
)
custom_field_doclink = CustomField.objects.create(
name="Test Custom Field Doc Link",
data_type=CustomField.FieldDataType.DOCUMENTLINK,
)
# Add links, creates bi-directional
resp = self.client.patch(
f"/api/documents/{doc1.id}/",
data={
"custom_fields": [
{
"field": custom_field_doclink.id,
"value": [2, 3, 4],
},
],
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(CustomFieldInstance.objects.count(), 4)
self.assertEqual(doc2.custom_fields.first().value, [1])
self.assertEqual(doc3.custom_fields.first().value, [1])
self.assertEqual(doc4.custom_fields.first().value, [1])
# Add links appends if necessary
resp = self.client.patch(
f"/api/documents/{doc3.id}/",
data={
"custom_fields": [
{
"field": custom_field_doclink.id,
"value": [1, 4],
},
],
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(doc4.custom_fields.first().value, [1, 3])
# Remove one of the links, removed on other doc
resp = self.client.patch(
f"/api/documents/{doc1.id}/",
data={
"custom_fields": [
{
"field": custom_field_doclink.id,
"value": [2, 3],
},
],
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(doc2.custom_fields.first().value, [1])
self.assertEqual(doc3.custom_fields.first().value, [1, 4])
self.assertEqual(doc4.custom_fields.first().value, [3])
# Removes the field entirely
resp = self.client.patch(
f"/api/documents/{doc1.id}/",
data={
"custom_fields": [],
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(doc2.custom_fields.first().value, [])
self.assertEqual(doc3.custom_fields.first().value, [4])
self.assertEqual(doc4.custom_fields.first().value, [3])

View File

@@ -594,7 +594,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
results = response.data["results"]
self.assertEqual(len(results), 0)
def test_document_owner_filters(self):
def test_document_permissions_filters(self):
"""
GIVEN:
- Documents with owners, with and without granted permissions
@@ -686,6 +686,18 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
[u1_doc1.id, u1_doc2.id, u2_doc2.id],
)
assign_perm("view_document", u2, u1_doc1)
# Will show only documents shared by user
response = self.client.get(f"/api/documents/?shared_by__id={u1.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data["results"]
self.assertEqual(len(results), 1)
self.assertCountEqual(
[results[0]["id"]],
[u1_doc1.id],
)
def test_pagination_all(self):
"""
GIVEN:

View File

@@ -408,10 +408,17 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
checksum="3",
owner=user2,
)
doc4 = Document.objects.create(
title="Test4",
content="content 4",
checksum="4",
owner=user1,
)
assign_perm("view_document", user1, doc2)
assign_perm("view_document", user1, doc3)
assign_perm("change_document", user1, doc3)
assign_perm("view_document", user2, doc4)
self.client.force_authenticate(user1)
@@ -426,9 +433,11 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
self.assertNotIn("permissions", resp_data["results"][0])
self.assertIn("user_can_change", resp_data["results"][0])
self.assertEqual(resp_data["results"][0]["user_can_change"], True) # doc1
self.assertEqual(resp_data["results"][1]["user_can_change"], False) # doc2
self.assertEqual(resp_data["results"][2]["user_can_change"], True) # doc3
self.assertTrue(resp_data["results"][0]["user_can_change"]) # doc1
self.assertFalse(resp_data["results"][0]["is_shared_by_requester"]) # doc1
self.assertFalse(resp_data["results"][1]["user_can_change"]) # doc2
self.assertTrue(resp_data["results"][2]["user_can_change"]) # doc3
self.assertTrue(resp_data["results"][3]["is_shared_by_requester"]) # doc4
response = self.client.get(
"/api/documents/?full_perms=true",
@@ -441,6 +450,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
self.assertIn("permissions", resp_data["results"][0])
self.assertNotIn("user_can_change", resp_data["results"][0])
self.assertNotIn("is_shared_by_requester", resp_data["results"][0])
class TestApiUser(DirectoriesMixin, APITestCase):

View File

@@ -968,7 +968,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
u1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
u2.user_permissions.add(*Permission.objects.filter(codename="view_document"))
Document.objects.create(checksum="1", content="test 1", owner=u1)
d1 = Document.objects.create(checksum="1", content="test 1", owner=u1)
d2 = Document.objects.create(checksum="2", content="test 2", owner=u2)
d3 = Document.objects.create(checksum="3", content="test 3", owner=u2)
Document.objects.create(checksum="4", content="test 4")
@@ -993,9 +993,10 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
assign_perm("view_document", u1, d2)
assign_perm("view_document", u1, d3)
assign_perm("view_document", u2, d1)
with AsyncWriter(index.open_index()) as writer:
for doc in [d2, d3]:
for doc in [d1, d2, d3]:
index.update_document(writer, doc)
self.client.force_authenticate(user=u1)
@@ -1011,6 +1012,8 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
self.assertEqual(r.data["count"], 1)
r = self.client.get("/api/documents/?query=test&owner__isnull=true")
self.assertEqual(r.data["count"], 1)
r = self.client.get(f"/api/documents/?query=test&shared_by__id={u1.id}")
self.assertEqual(r.data["count"], 1)
def test_search_sorting(self):
u1 = User.objects.create_user("user1")

View File

@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-05 08:26-0800\n"
"POT-Creation-Date: 2023-12-09 10:53-0800\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
@@ -21,7 +21,7 @@ msgstr ""
msgid "Documents"
msgstr ""
#: documents/models.py:36 documents/models.py:734
#: documents/models.py:36 documents/models.py:736
msgid "owner"
msgstr ""
@@ -53,7 +53,7 @@ msgstr ""
msgid "Automatic"
msgstr ""
#: documents/models.py:62 documents/models.py:402 documents/models.py:895
#: documents/models.py:62 documents/models.py:402 documents/models.py:897
#: paperless_mail/models.py:18 paperless_mail/models.py:93
msgid "name"
msgstr ""
@@ -132,7 +132,7 @@ msgstr ""
msgid "title"
msgstr ""
#: documents/models.py:171 documents/models.py:648
#: documents/models.py:171 documents/models.py:650
msgid "content"
msgstr ""
@@ -162,8 +162,8 @@ msgstr ""
msgid "The checksum of the archived document."
msgstr ""
#: documents/models.py:205 documents/models.py:385 documents/models.py:654
#: documents/models.py:692 documents/models.py:762 documents/models.py:799
#: documents/models.py:205 documents/models.py:385 documents/models.py:656
#: documents/models.py:694 documents/models.py:764 documents/models.py:801
msgid "created"
msgstr ""
@@ -211,7 +211,7 @@ msgstr ""
msgid "The position of this document in your physical document archive."
msgstr ""
#: documents/models.py:279 documents/models.py:665 documents/models.py:719
#: documents/models.py:279 documents/models.py:667 documents/models.py:721
msgid "document"
msgstr ""
@@ -259,7 +259,7 @@ msgstr ""
msgid "logs"
msgstr ""
#: documents/models.py:399 documents/models.py:464
#: documents/models.py:399 documents/models.py:466
msgid "saved view"
msgstr ""
@@ -427,298 +427,306 @@ msgstr ""
msgid "does not have owner in"
msgstr ""
#: documents/models.py:467
msgid "rule type"
#: documents/models.py:458
msgid "has custom field value"
msgstr ""
#: documents/models.py:459
msgid "is shared by me"
msgstr ""
#: documents/models.py:469
msgid "rule type"
msgstr ""
#: documents/models.py:471
msgid "value"
msgstr ""
#: documents/models.py:472
#: documents/models.py:474
msgid "filter rule"
msgstr ""
#: documents/models.py:473
#: documents/models.py:475
msgid "filter rules"
msgstr ""
#: documents/models.py:584
#: documents/models.py:586
msgid "Task ID"
msgstr ""
#: documents/models.py:585
#: documents/models.py:587
msgid "Celery ID for the Task that was run"
msgstr ""
#: documents/models.py:590
#: documents/models.py:592
msgid "Acknowledged"
msgstr ""
#: documents/models.py:591
#: documents/models.py:593
msgid "If the task is acknowledged via the frontend or API"
msgstr ""
#: documents/models.py:597
#: documents/models.py:599
msgid "Task Filename"
msgstr ""
#: documents/models.py:598
#: documents/models.py:600
msgid "Name of the file which the Task was run for"
msgstr ""
#: documents/models.py:604
#: documents/models.py:606
msgid "Task Name"
msgstr ""
#: documents/models.py:605
#: documents/models.py:607
msgid "Name of the Task which was run"
msgstr ""
#: documents/models.py:612
#: documents/models.py:614
msgid "Task State"
msgstr ""
#: documents/models.py:613
#: documents/models.py:615
msgid "Current state of the task being run"
msgstr ""
#: documents/models.py:618
#: documents/models.py:620
msgid "Created DateTime"
msgstr ""
#: documents/models.py:619
#: documents/models.py:621
msgid "Datetime field when the task result was created in UTC"
msgstr ""
#: documents/models.py:624
#: documents/models.py:626
msgid "Started DateTime"
msgstr ""
#: documents/models.py:625
#: documents/models.py:627
msgid "Datetime field when the task was started in UTC"
msgstr ""
#: documents/models.py:630
#: documents/models.py:632
msgid "Completed DateTime"
msgstr ""
#: documents/models.py:631
#: documents/models.py:633
msgid "Datetime field when the task was completed in UTC"
msgstr ""
#: documents/models.py:636
#: documents/models.py:638
msgid "Result Data"
msgstr ""
#: documents/models.py:638
#: documents/models.py:640
msgid "The data returned by the task"
msgstr ""
#: documents/models.py:650
#: documents/models.py:652
msgid "Note for the document"
msgstr ""
#: documents/models.py:674
#: documents/models.py:676
msgid "user"
msgstr ""
#: documents/models.py:679
#: documents/models.py:681
msgid "note"
msgstr ""
#: documents/models.py:680
#: documents/models.py:682
msgid "notes"
msgstr ""
#: documents/models.py:688
#: documents/models.py:690
msgid "Archive"
msgstr ""
#: documents/models.py:689
#: documents/models.py:691
msgid "Original"
msgstr ""
#: documents/models.py:700
#: documents/models.py:702
msgid "expiration"
msgstr ""
#: documents/models.py:707
#: documents/models.py:709
msgid "slug"
msgstr ""
#: documents/models.py:739
#: documents/models.py:741
msgid "share link"
msgstr ""
#: documents/models.py:740
#: documents/models.py:742
msgid "share links"
msgstr ""
#: documents/models.py:752
#: documents/models.py:754
msgid "String"
msgstr ""
#: documents/models.py:753
#: documents/models.py:755
msgid "URL"
msgstr ""
#: documents/models.py:754
#: documents/models.py:756
msgid "Date"
msgstr ""
#: documents/models.py:755
#: documents/models.py:757
msgid "Boolean"
msgstr ""
#: documents/models.py:756
#: documents/models.py:758
msgid "Integer"
msgstr ""
#: documents/models.py:757
#: documents/models.py:759
msgid "Float"
msgstr ""
#: documents/models.py:758
#: documents/models.py:760
msgid "Monetary"
msgstr ""
#: documents/models.py:759
#: documents/models.py:761
msgid "Document Link"
msgstr ""
#: documents/models.py:771
#: documents/models.py:773
msgid "data type"
msgstr ""
#: documents/models.py:779
#: documents/models.py:781
msgid "custom field"
msgstr ""
#: documents/models.py:780
#: documents/models.py:782
msgid "custom fields"
msgstr ""
#: documents/models.py:842
#: documents/models.py:844
msgid "custom field instance"
msgstr ""
#: documents/models.py:843
#: documents/models.py:845
msgid "custom field instances"
msgstr ""
#: documents/models.py:891
#: documents/models.py:893
msgid "Consume Folder"
msgstr ""
#: documents/models.py:892
#: documents/models.py:894
msgid "Api Upload"
msgstr ""
#: documents/models.py:893
#: documents/models.py:895
msgid "Mail Fetch"
msgstr ""
#: documents/models.py:897 paperless_mail/models.py:95
#: documents/models.py:899 paperless_mail/models.py:95
msgid "order"
msgstr ""
#: documents/models.py:906
#: documents/models.py:908
msgid "filter path"
msgstr ""
#: documents/models.py:911
#: documents/models.py:913
msgid ""
"Only consume documents with a path that matches this if specified. Wildcards "
"specified as * are allowed. Case insensitive."
msgstr ""
#: documents/models.py:918
#: documents/models.py:920
msgid "filter filename"
msgstr ""
#: documents/models.py:923 paperless_mail/models.py:148
#: documents/models.py:925 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:934
#: documents/models.py:936
msgid "filter documents from this mail rule"
msgstr ""
#: documents/models.py:938
#: documents/models.py:940
msgid "assign title"
msgstr ""
#: documents/models.py:943
#: documents/models.py:945
msgid ""
"Assign a document title, can include some placeholders, see documentation."
msgstr ""
#: documents/models.py:951 paperless_mail/models.py:216
#: documents/models.py:953 paperless_mail/models.py:216
msgid "assign this tag"
msgstr ""
#: documents/models.py:959 paperless_mail/models.py:224
#: documents/models.py:961 paperless_mail/models.py:224
msgid "assign this document type"
msgstr ""
#: documents/models.py:967 paperless_mail/models.py:238
#: documents/models.py:969 paperless_mail/models.py:238
msgid "assign this correspondent"
msgstr ""
#: documents/models.py:975
#: documents/models.py:977
msgid "assign this storage path"
msgstr ""
#: documents/models.py:984
#: documents/models.py:986
msgid "assign this owner"
msgstr ""
#: documents/models.py:991
#: documents/models.py:993
msgid "grant view permissions to these users"
msgstr ""
#: documents/models.py:998
#: documents/models.py:1000
msgid "grant view permissions to these groups"
msgstr ""
#: documents/models.py:1005
#: documents/models.py:1007
msgid "grant change permissions to these users"
msgstr ""
#: documents/models.py:1012
#: documents/models.py:1014
msgid "grant change permissions to these groups"
msgstr ""
#: documents/models.py:1019
#: documents/models.py:1021
msgid "assign these custom fields"
msgstr ""
#: documents/models.py:1023
#: documents/models.py:1025
msgid "consumption template"
msgstr ""
#: documents/models.py:1024
#: documents/models.py:1026
msgid "consumption templates"
msgstr ""
#: documents/serialisers.py:102
#: documents/serialisers.py:105
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr ""
#: documents/serialisers.py:377
#: documents/serialisers.py:399
msgid "Invalid color."
msgstr ""
#: documents/serialisers.py:842
#: documents/serialisers.py:865
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
#: documents/serialisers.py:939
#: documents/serialisers.py:962
msgid "Invalid variable detected."
msgstr ""

View File

@@ -119,6 +119,10 @@ class MailRuleAdmin(GuardedModelAdmin):
ordering = ["order"]
raw_id_fields = ("assign_correspondent", "assign_document_type")
filter_horizontal = ("assign_tags",)
class ProcessedMailAdmin(admin.ModelAdmin):
class Meta: