First backend bits, savedviews fully permissions-capable

This commit is contained in:
shamoon
2026-02-20 08:38:09 -08:00
parent eda0e61cec
commit e0e517358d
4 changed files with 75 additions and 40 deletions

View File

@@ -1394,6 +1394,7 @@ class SavedViewSerializer(OwnedObjectSerializer):
"owner", "owner",
"permissions", "permissions",
"user_can_change", "user_can_change",
"set_permissions",
] ]
def validate(self, attrs): def validate(self, attrs):
@@ -1431,7 +1432,7 @@ class SavedViewSerializer(OwnedObjectSerializer):
and len(validated_data["display_fields"]) == 0 and len(validated_data["display_fields"]) == 0
): ):
validated_data["display_fields"] = None validated_data["display_fields"] = None
super().update(instance, validated_data) instance = super().update(instance, validated_data)
if rules_data is not None: if rules_data is not None:
SavedViewFilterRule.objects.filter(saved_view=instance).delete() SavedViewFilterRule.objects.filter(saved_view=instance).delete()
for rule_data in rules_data: for rule_data in rules_data:
@@ -1443,7 +1444,7 @@ class SavedViewSerializer(OwnedObjectSerializer):
if "user" in validated_data: if "user" in validated_data:
# backwards compatibility # backwards compatibility
validated_data["owner"] = validated_data.pop("user") 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: for rule_data in rules_data:
SavedViewFilterRule.objects.create(saved_view=saved_view, **rule_data) SavedViewFilterRule.objects.create(saved_view=saved_view, **rule_data)
return saved_view return saved_view

View File

@@ -2014,8 +2014,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
mock_get_date_parser.assert_not_called() mock_get_date_parser.assert_not_called()
def test_saved_views(self) -> None: def test_saved_views(self) -> None:
u1 = User.objects.create_superuser("user1") u1 = User.objects.create_user("user1")
u2 = User.objects.create_superuser("user2") 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( v1 = SavedView.objects.create(
owner=u1, owner=u1,
@@ -2024,14 +2030,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
show_on_dashboard=False, show_on_dashboard=False,
show_in_sidebar=False, show_in_sidebar=False,
) )
SavedView.objects.create( v2 = SavedView.objects.create(
owner=u2, owner=u2,
name="test2", name="test2",
sort_field="", sort_field="",
show_on_dashboard=False, show_on_dashboard=False,
show_in_sidebar=False, show_in_sidebar=False,
) )
SavedView.objects.create( v3 = SavedView.objects.create(
owner=u2, owner=u2,
name="test3", name="test3",
sort_field="", sort_field="",
@@ -2039,36 +2045,62 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
show_in_sidebar=False, show_in_sidebar=False,
) )
response = self.client.get("/api/saved_views/") assign_perm("view_savedview", u1, v2)
self.assertEqual(response.status_code, status.HTTP_200_OK) assign_perm("change_savedview", u1, v2)
self.assertEqual(response.data["count"], 0) assign_perm("view_savedview", u1, v3)
self.assertEqual(
self.client.get(f"/api/saved_views/{v1.id}/").status_code,
status.HTTP_404_NOT_FOUND,
)
self.client.force_authenticate(user=u1) self.client.force_authenticate(user=u1)
response = self.client.get("/api/saved_views/") response = self.client.get("/api/saved_views/")
self.assertEqual(response.status_code, status.HTTP_200_OK) 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.assertEqual(
self.client.get(f"/api/saved_views/{v1.id}/").status_code, response.status_code,
status.HTTP_200_OK, 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/") response = self.client.get("/api/saved_views/")
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["count"], 2) self.assertEqual(response.data["count"], 0)
self.assertEqual(
self.client.get(f"/api/saved_views/{v1.id}/").status_code,
status.HTTP_404_NOT_FOUND,
)
def test_saved_view_create_update_patch(self) -> None: def test_saved_view_create_update_patch(self) -> None:
User.objects.create_user("user1") User.objects.create_user("user1")

View File

@@ -1307,13 +1307,14 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
tag1 = Tag.objects.create(name="bank tag1") tag1 = Tag.objects.create(name="bank tag1")
Tag.objects.create(name="tag2") Tag.objects.create(name="tag2")
SavedView.objects.create( shared_view = SavedView.objects.create(
name="bank view", name="bank view",
show_on_dashboard=True, show_on_dashboard=True,
show_in_sidebar=True, show_in_sidebar=True,
sort_field="", sort_field="",
owner=user1, owner=user2,
) )
assign_perm("view_savedview", user1, shared_view)
mail_account1 = MailAccount.objects.create(name="bank mail account 1") mail_account1 = MailAccount.objects.create(name="bank mail account 1")
mail_account2 = MailAccount.objects.create(name="mail account 2") mail_account2 = MailAccount.objects.create(name="mail account 2")
mail_rule1 = MailRule.objects.create( mail_rule1 = MailRule.objects.create(

View File

@@ -1660,24 +1660,21 @@ class LogViewSet(ViewSet):
return Response(existing_logs) return Response(existing_logs)
class SavedViewViewSet(ModelViewSet, PassUserMixin): @extend_schema_view(**generate_object_with_permissions_schema(SavedViewSerializer))
class SavedViewViewSet(BulkPermissionMixin, PassUserMixin, ModelViewSet):
model = SavedView model = SavedView
queryset = SavedView.objects.all() queryset = SavedView.objects.select_related("owner").prefetch_related(
"filter_rules",
)
serializer_class = SavedViewSerializer serializer_class = SavedViewSerializer
pagination_class = StandardPagination pagination_class = StandardPagination
permission_classes = (IsAuthenticated, PaperlessObjectPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = (
def get_queryset(self): OrderingFilter,
user = self.request.user ObjectOwnedOrGrantedPermissionsFilter,
return ( )
SavedView.objects.filter(owner=user) ordering_fields = ("name", "show_on_dashboard", "show_in_sidebar")
.select_related("owner")
.prefetch_related("filter_rules")
)
def perform_create(self, serializer) -> None:
serializer.save(owner=self.request.user)
@extend_schema_view( @extend_schema_view(
@@ -2201,7 +2198,11 @@ class GlobalSearchView(PassUserMixin):
) )
docs = docs[:OBJECT_LIMIT] docs = docs[:OBJECT_LIMIT]
saved_views = ( 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") if request.user.has_perm("documents.view_savedview")
else [] else []
) )