diff --git a/src/documents/filters.py b/src/documents/filters.py index 56a490bc0..53ef0391c 100644 --- a/src/documents/filters.py +++ b/src/documents/filters.py @@ -105,6 +105,8 @@ class DocumentFilterSet(FilterSet): title_content = TitleContentFilter() + owner__id__none = ObjectFilter(field_name="owner", exclude=True) + class Meta: model = Document fields = { @@ -125,6 +127,8 @@ class DocumentFilterSet(FilterSet): "storage_path": ["isnull"], "storage_path__id": ID_KWARGS, "storage_path__name": CHAR_KWARGS, + "owner": ["isnull"], + "owner__id": ID_KWARGS, } diff --git a/src/documents/index.py b/src/documents/index.py index 0b0493514..540bce9d5 100644 --- a/src/documents/index.py +++ b/src/documents/index.py @@ -208,11 +208,13 @@ class DelayedQuery: for document_type_id in v.split(","): criterias.append(query.Not(query.Term("type_id", document_type_id))) elif k == "correspondent__isnull": - criterias.append(query.Term("has_correspondent", v == "false")) + criterias.append( + query.Term("has_correspondent", self.evalBoolean(v)), + ) elif k == "is_tagged": - criterias.append(query.Term("has_tag", v == "true")) + criterias.append(query.Term("has_tag", self.evalBoolean(v))) elif k == "document_type__isnull": - criterias.append(query.Term("has_type", v == "false")) + criterias.append(query.Term("has_type", self.evalBoolean(v) is False)) elif k == "created__date__lt": criterias.append( query.DateRange("created", start=None, end=isoparse(v)), @@ -236,7 +238,19 @@ class DelayedQuery: for storage_path_id in v.split(","): criterias.append(query.Not(query.Term("path_id", storage_path_id))) elif k == "storage_path__isnull": - criterias.append(query.Term("has_path", v == "false")) + criterias.append(query.Term("has_path", self.evalBoolean(v) is False)) + elif k == "owner__isnull": + criterias.append(query.Term("has_owner", self.evalBoolean(v) is False)) + elif k == "owner__id": + criterias.append(query.Term("owner_id", v)) + elif k == "owner__id__in": + owners_in = [] + for owner_id in v.split(","): + owners_in.append(query.Term("owner_id", owner_id)) + criterias.append(query.Or(owners_in)) + elif k == "owner__id__none": + for owner_id in v.split(","): + criterias.append(query.Not(query.Term("owner_id", owner_id))) user_criterias = [query.Term("has_owner", False)] if "user" in self.query_params: @@ -254,6 +268,12 @@ class DelayedQuery: else: return query.Or(user_criterias) if len(user_criterias) > 0 else None + def evalBoolean(self, val): + if val == "false" or val == "0": + return False + if val == "true" or val == "1": + return True + def _get_query_sortedby(self): if "ordering" not in self.query_params: return None, False @@ -269,6 +289,7 @@ class DelayedQuery: "document_type__name": "type", "archive_serial_number": "asn", "num_notes": "num_notes", + "owner": "owner", } if field.startswith("-"): diff --git a/src/documents/migrations/1036_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1036_alter_savedviewfilterrule_rule_type.py new file mode 100644 index 000000000..e65586ad8 --- /dev/null +++ b/src/documents/migrations/1036_alter_savedviewfilterrule_rule_type.py @@ -0,0 +1,58 @@ +# Generated by Django 4.1.7 on 2023-05-04 04:11 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "1035_rename_comment_note"), + ] + + 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"), + ], + verbose_name="rule type", + ), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index d28664a68..e3f14a8b2 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -448,6 +448,10 @@ class SavedViewFilterRule(models.Model): (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")), ] saved_view = models.ForeignKey( diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index dd872fe78..416a0adfd 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -469,6 +469,98 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): results = response.data["results"] self.assertEqual(len(results), 0) + def test_document_owner_filters(self): + """ + GIVEN: + - Documents with owners, with and without granted permissions + WHEN: + - User filters by owner + THEN: + - Owner filters work correctly but still respect permissions + """ + u1 = User.objects.create_user("user1") + u2 = User.objects.create_user("user2") + u1.user_permissions.add(*Permission.objects.filter(codename="view_document")) + u2.user_permissions.add(*Permission.objects.filter(codename="view_document")) + + u1_doc1 = Document.objects.create( + title="none1", + checksum="A", + mime_type="application/pdf", + owner=u1, + ) + Document.objects.create( + title="none2", + checksum="B", + mime_type="application/pdf", + owner=u2, + ) + u0_doc1 = Document.objects.create( + title="none3", + checksum="C", + mime_type="application/pdf", + ) + u1_doc2 = Document.objects.create( + title="none4", + checksum="D", + mime_type="application/pdf", + owner=u1, + ) + u2_doc2 = Document.objects.create( + title="none5", + checksum="E", + mime_type="application/pdf", + owner=u2, + ) + + self.client.force_authenticate(user=u1) + assign_perm("view_document", u1, u2_doc2) + + # Will not show any u1 docs or u2_doc1 which isn't shared + response = self.client.get(f"/api/documents/?owner__id__none={u1.id}") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 2) + self.assertCountEqual( + [results[0]["id"], results[1]["id"]], + [u0_doc1.id, u2_doc2.id], + ) + + # Will not show any u1 docs, u0_doc1 which has no owner or u2_doc1 which isn't shared + response = self.client.get( + f"/api/documents/?owner__id__none={u1.id}&owner__isnull=false", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertCountEqual([results[0]["id"]], [u2_doc2.id]) + + # Will not show any u1 docs, u2_doc2 which is shared but has owner + response = self.client.get( + f"/api/documents/?owner__id__none={u1.id}&owner__isnull=true", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertCountEqual([results[0]["id"]], [u0_doc1.id]) + + # Will not show any u1 docs or u2_doc1 which is not shared + response = self.client.get(f"/api/documents/?owner__id={u2.id}") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertCountEqual([results[0]["id"]], [u2_doc2.id]) + + # Will not show u2_doc1 which is not shared + response = self.client.get(f"/api/documents/?owner__id__in={u1.id},{u2.id}") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 3) + self.assertCountEqual( + [results[0]["id"], results[1]["id"], results[2]["id"]], + [u1_doc1.id, u1_doc2.id, u2_doc2.id], + ) + def test_search(self): d1 = Document.objects.create( title="invoice", @@ -1112,18 +1204,30 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): self.assertEqual(r.data["count"], 2) r = self.client.get("/api/documents/?query=test&document_type__id__none=1") self.assertEqual(r.data["count"], 2) + r = self.client.get(f"/api/documents/?query=test&owner__id__none={u1.id}") + self.assertEqual(r.data["count"], 1) + r = self.client.get(f"/api/documents/?query=test&owner__id__in={u1.id}") + self.assertEqual(r.data["count"], 1) + r = self.client.get( + f"/api/documents/?query=test&owner__id__none={u1.id}&owner__isnull=true", + ) + self.assertEqual(r.data["count"], 1) self.client.force_authenticate(user=u2) r = self.client.get("/api/documents/?query=test") self.assertEqual(r.data["count"], 3) r = self.client.get("/api/documents/?query=test&document_type__id__none=1") self.assertEqual(r.data["count"], 3) + r = self.client.get(f"/api/documents/?query=test&owner__id__none={u2.id}") + self.assertEqual(r.data["count"], 1) self.client.force_authenticate(user=superuser) r = self.client.get("/api/documents/?query=test") self.assertEqual(r.data["count"], 4) r = self.client.get("/api/documents/?query=test&document_type__id__none=1") self.assertEqual(r.data["count"], 4) + r = self.client.get(f"/api/documents/?query=test&owner__id__none={u1.id}") + self.assertEqual(r.data["count"], 3) def test_search_filtering_with_object_perms(self): """ @@ -1153,6 +1257,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): self.assertEqual(r.data["count"], 2) r = self.client.get("/api/documents/?query=test&document_type__id__none=1") self.assertEqual(r.data["count"], 2) + r = self.client.get(f"/api/documents/?query=test&owner__id__none={u1.id}") + self.assertEqual(r.data["count"], 1) + r = self.client.get(f"/api/documents/?query=test&owner__id={u1.id}") + self.assertEqual(r.data["count"], 1) + r = self.client.get(f"/api/documents/?query=test&owner__id__in={u1.id}") + self.assertEqual(r.data["count"], 1) + r = self.client.get("/api/documents/?query=test&owner__isnull=true") + self.assertEqual(r.data["count"], 1) assign_perm("view_document", u1, d2) assign_perm("view_document", u1, d3) @@ -1166,6 +1278,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): self.assertEqual(r.data["count"], 4) r = self.client.get("/api/documents/?query=test&document_type__id__none=1") self.assertEqual(r.data["count"], 4) + r = self.client.get(f"/api/documents/?query=test&owner__id__none={u1.id}") + self.assertEqual(r.data["count"], 3) + r = self.client.get(f"/api/documents/?query=test&owner__id={u1.id}") + self.assertEqual(r.data["count"], 1) + r = self.client.get(f"/api/documents/?query=test&owner__id__in={u1.id}") + self.assertEqual(r.data["count"], 1) + r = self.client.get("/api/documents/?query=test&owner__isnull=true") + self.assertEqual(r.data["count"], 1) def test_search_sorting(self): u1 = User.objects.create_user("user1") @@ -1247,6 +1367,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): search_query("&ordering=-num_notes"), [d1.id, d3.id, d2.id], ) + self.assertListEqual( + search_query("&ordering=owner"), + [d1.id, d2.id, d3.id], + ) + self.assertListEqual( + search_query("&ordering=-owner"), + [d3.id, d2.id, d1.id], + ) def test_statistics(self): doc1 = Document.objects.create( diff --git a/src/documents/views.py b/src/documents/views.py index bfe2b3e6f..e859571f2 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -264,6 +264,7 @@ class DocumentViewSet( "added", "archive_serial_number", "num_notes", + "owner", ) def get_queryset(self):