mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -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")
|
||||
|
Reference in New Issue
Block a user