mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Enhancement: shared icon & shared by me filter (#4859)
This commit is contained in:
		| @@ -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 = { | ||||
|   | ||||
| @@ -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(","): | ||||
|   | ||||
| @@ -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", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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", | ||||
| @@ -556,6 +578,7 @@ class DocumentSerializer( | ||||
|             "owner", | ||||
|             "permissions", | ||||
|             "user_can_change", | ||||
|             "is_shared_by_requester", | ||||
|             "set_permissions", | ||||
|             "notes", | ||||
|             "custom_fields", | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -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 "" | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon