From e0e517358d0cbdebcdd018f75039e808d3b528b5 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Fri, 20 Feb 2026 08:38:09 -0800 Subject: [PATCH] First backend bits, savedviews fully permissions-capable --- src/documents/serialisers.py | 5 +- src/documents/tests/test_api_documents.py | 76 ++++++++++++++++------- src/documents/tests/test_api_search.py | 5 +- src/documents/views.py | 29 ++++----- 4 files changed, 75 insertions(+), 40 deletions(-) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index e16ad76de..f8258150c 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -1394,6 +1394,7 @@ class SavedViewSerializer(OwnedObjectSerializer): "owner", "permissions", "user_can_change", + "set_permissions", ] def validate(self, attrs): @@ -1431,7 +1432,7 @@ class SavedViewSerializer(OwnedObjectSerializer): and len(validated_data["display_fields"]) == 0 ): validated_data["display_fields"] = None - super().update(instance, validated_data) + instance = super().update(instance, validated_data) if rules_data is not None: SavedViewFilterRule.objects.filter(saved_view=instance).delete() for rule_data in rules_data: @@ -1443,7 +1444,7 @@ class SavedViewSerializer(OwnedObjectSerializer): if "user" in validated_data: # backwards compatibility validated_data["owner"] = validated_data.pop("user") - saved_view = SavedView.objects.create(**validated_data) + saved_view = super().create(validated_data) for rule_data in rules_data: SavedViewFilterRule.objects.create(saved_view=saved_view, **rule_data) return saved_view diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index c362f9646..23ae7c3b0 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -2014,8 +2014,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): mock_get_date_parser.assert_not_called() def test_saved_views(self) -> None: - u1 = User.objects.create_superuser("user1") - u2 = User.objects.create_superuser("user2") + u1 = User.objects.create_user("user1") + u2 = User.objects.create_user("user2") + u3 = User.objects.create_user("user3") + + view_perm = Permission.objects.get(codename="view_savedview") + change_perm = Permission.objects.get(codename="change_savedview") + for user in [u1, u2, u3]: + user.user_permissions.add(view_perm, change_perm) v1 = SavedView.objects.create( owner=u1, @@ -2024,14 +2030,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): show_on_dashboard=False, show_in_sidebar=False, ) - SavedView.objects.create( + v2 = SavedView.objects.create( owner=u2, name="test2", sort_field="", show_on_dashboard=False, show_in_sidebar=False, ) - SavedView.objects.create( + v3 = SavedView.objects.create( owner=u2, name="test3", sort_field="", @@ -2039,36 +2045,62 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): show_in_sidebar=False, ) - response = self.client.get("/api/saved_views/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 0) - - self.assertEqual( - self.client.get(f"/api/saved_views/{v1.id}/").status_code, - status.HTTP_404_NOT_FOUND, - ) + assign_perm("view_savedview", u1, v2) + assign_perm("change_savedview", u1, v2) + assign_perm("view_savedview", u1, v3) self.client.force_authenticate(user=u1) response = self.client.get("/api/saved_views/") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 1) + self.assertEqual(response.data["count"], 3) + for view_id in [v1.id, v2.id, v3.id]: + self.assertEqual( + self.client.get(f"/api/saved_views/{view_id}/").status_code, + status.HTTP_200_OK, + ) + + response = self.client.patch( + f"/api/saved_views/{v2.id}/", + {"show_in_sidebar": True}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.patch( + f"/api/saved_views/{v3.id}/", + {"show_in_sidebar": True}, + format="json", + ) self.assertEqual( - self.client.get(f"/api/saved_views/{v1.id}/").status_code, - status.HTTP_200_OK, + response.status_code, + status.HTTP_403_FORBIDDEN, ) - self.client.force_authenticate(user=u2) + response = self.client.patch( + f"/api/saved_views/{v2.id}/", + { + "set_permissions": { + "view": {"users": [u3.id]}, + }, + }, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + response = self.client.patch( + f"/api/saved_views/{v2.id}/", + {"owner": u1.id}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + self.client.force_authenticate(user=u3) response = self.client.get("/api/saved_views/") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 2) - - self.assertEqual( - self.client.get(f"/api/saved_views/{v1.id}/").status_code, - status.HTTP_404_NOT_FOUND, - ) + self.assertEqual(response.data["count"], 0) def test_saved_view_create_update_patch(self) -> None: User.objects.create_user("user1") diff --git a/src/documents/tests/test_api_search.py b/src/documents/tests/test_api_search.py index 2aa3f1ae7..34d097f73 100644 --- a/src/documents/tests/test_api_search.py +++ b/src/documents/tests/test_api_search.py @@ -1307,13 +1307,14 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): tag1 = Tag.objects.create(name="bank tag1") Tag.objects.create(name="tag2") - SavedView.objects.create( + shared_view = SavedView.objects.create( name="bank view", show_on_dashboard=True, show_in_sidebar=True, sort_field="", - owner=user1, + owner=user2, ) + assign_perm("view_savedview", user1, shared_view) mail_account1 = MailAccount.objects.create(name="bank mail account 1") mail_account2 = MailAccount.objects.create(name="mail account 2") mail_rule1 = MailRule.objects.create( diff --git a/src/documents/views.py b/src/documents/views.py index b2fd118e5..e77e49020 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -1660,24 +1660,21 @@ class LogViewSet(ViewSet): return Response(existing_logs) -class SavedViewViewSet(ModelViewSet, PassUserMixin): +@extend_schema_view(**generate_object_with_permissions_schema(SavedViewSerializer)) +class SavedViewViewSet(BulkPermissionMixin, PassUserMixin, ModelViewSet): model = SavedView - queryset = SavedView.objects.all() + queryset = SavedView.objects.select_related("owner").prefetch_related( + "filter_rules", + ) serializer_class = SavedViewSerializer pagination_class = StandardPagination permission_classes = (IsAuthenticated, PaperlessObjectPermissions) - - def get_queryset(self): - user = self.request.user - return ( - SavedView.objects.filter(owner=user) - .select_related("owner") - .prefetch_related("filter_rules") - ) - - def perform_create(self, serializer) -> None: - serializer.save(owner=self.request.user) + filter_backends = ( + OrderingFilter, + ObjectOwnedOrGrantedPermissionsFilter, + ) + ordering_fields = ("name", "show_on_dashboard", "show_in_sidebar") @extend_schema_view( @@ -2201,7 +2198,11 @@ class GlobalSearchView(PassUserMixin): ) docs = docs[:OBJECT_LIMIT] saved_views = ( - SavedView.objects.filter(owner=request.user, name__icontains=query) + get_objects_for_user_owner_aware( + request.user, + "view_savedview", + SavedView, + ).filter(name__icontains=query) if request.user.has_perm("documents.view_savedview") else [] )