8000 GenericAPIView now applies filter_backend for list and retrieve api v… · martync/django-rest-framework@1a8f07d · GitHub
[go: up one dir, main page]

Skip to content

Commit 1a8f07d

Browse files
committed
GenericAPIView now applies filter_backend for list and retrieve api views
Before this commit only the MultipleObjectAPIView would apply a filter_backend, leaving the SingleObjectAPIView to return objects you might otherwise expect to have been filtered out. It's worth mentioning that when a SingleObjectAPIView makes a request for an object that should be excluded, a 404 is the expected result.
1 parent 4e80541 commit 1a8f07d

File tree

3 files changed

+88
-11
lines changed

3 files changed

+88
-11
lines changed

rest_framework/generics.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ class GenericAPIView(views.APIView):
1818
model = None
1919
serializer_class = None
2020
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
21+
filter_backend = api_settings.FILTER_BACKEND
22+
23+
def filter_queryset(self, queryset):
24+
"""
25+
Given a queryset, filter it with whichever filter backend is in use.
26+
"""
27+
if not self.filter_backend:
28+
return queryset
29+
backend = self.filter_backend()
30+
return backend.filter_queryset(self.request, queryset, self)
2131

2232
def get_serializer_context(self):
2333
"""
@@ -81,16 +91,6 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
8191
paginate_by = api_settings.PAGINATE_BY
8292
paginate_by_param = api_settings.PAGINATE_BY_PARAM
8393
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
84-
filter_backend = api_settings.FILTER_BACKEND
85-
86-
def filter_queryset(self, queryset):
87-
"""
88-
Given a queryset, filter it with whichever filter backend is in use.
89-
"""
90-
if not self.filter_backend:
91-
return queryset
92-
backend = self.filter_backend()
93-
return backend.filter_queryset(self.request, queryset, self)
9494

9595
def get_pagination_serializer(self, page=None):
9696
"""

rest_framework/mixins.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ class RetrieveModelMixin(object):
9797
Should be mixed in with `SingleObjectAPIView`.
9898
"""
9999
def retrieve(self, request, *args, **kwargs):
100-
self.object = self.get_object()
100+
queryset = self.get_queryset()
101+
filtered_queryset = self.filter_queryset(queryset)
102+
self.object = self.get_object(filtered_queryset)
101103
serializer = self.get_serializer(self.object)
102104
return Response(serializer.data)
103105

rest_framework/tests/generics.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,3 +350,78 @@ def test_m2m_in_browseable_api(self):
350350
view = ExampleView().as_view()
351351
response = view(request).render()
352352
self.assertEqual(response.status_code, status.HTTP_200_OK)
353+
354+
355+
class InclusiveFilterBackend(object):
356+
def filter_queryset(self, request, queryset, view):
357+
return queryset.filter(text='foo')
358+
359+
360+
class ExclusiveFilterBackend(object):
361+
def filter_queryset(self, request, queryset, view):
362+
return queryset.filter(text='other')
363+
364+
365+
class TestFilterBackendAppliedToViews(TestCase):
366+
367+
def setUp(self):
368+
"""
369+
Create 3 BasicModel instances to filter on.
370+
"""
371+
items = ['foo', 'bar', 'baz']
372+
for item in items:
373+
BasicModel(text=item).save()
374+
self.objects = BasicModel.objects
375+
10000 self.data = [
376+
{'id': obj.id, 'text': obj.text}
377+
for obj in self.objects.all()
378+
]
379+
self.root_view = RootView.as_view()
380+
self.instance_view = InstanceView.as_view()
381+
self.original_root_backend = getattr(RootView, 'filter_backend')
382+
self.original_instance_backend = getattr(InstanceView, 'filter_backend')
383+
384+
def tearDown(self):
385+
setattr(RootView, 'filter_backend', self.original_root_backend)
386+
setattr(InstanceView, 'filter_backend', self.original_instance_backend)
387+
388+
def test_get_root_view_filters_by_name_with_filter_backend(self):
389+
"""
390+
GET requests to ListCreateAPIView should return filtered list.
391+
"""
392+
setattr(RootView, 'filter_backend', InclusiveFilterBackend)
393+
request = factory.get('/')
394+
response = self.root_view(request).render()
395+
self.assertEqual(response.status_code, status.HTTP_200_OK)
396+
self.assertEqual(len(response.data), 1)
397+
self.assertEqual(response.data, [{'id': 1, 'text': 'foo'}])
398+
399+
def test_get_root_view_filters_out_all_models_with_exclusive_filter_backend(self):
400+
"""
401+
GET requests to ListCreateAPIView should return empty list when all models are filtered out.
402+
"""
403+
setattr(RootView, 'filter_backend', ExclusiveFilterBackend)
404+
request = factory.get('/')
405+
response = self.root_view(request).render()
406+
self.assertEqual(response.status_code, status.HTTP_200_OK)
407+
self.assertEqual(response.data, [])
408+
409+
def test_get_instance_view_filters_out_name_with_filter_backend(self):
410+
"""
411+
GET requests to RetrieveUpdateDestroyAPIView should raise 404 when model filtered out.
412+
"""
413+
setattr(InstanceView, 'filter_backend', ExclusiveFilterBackend)
414+
request = factory.get('/1')
415+
response = self.instance_view(request, pk=1).render()
416+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
417+
self.assertEqual(response.data, {'detail': 'Not found'})
418+
419+
def test_get_instance_view_will_return_single_object_when_filter_does_not_exclude_it(self):
420+
"""
421+
GET requests to RetrieveUpdateDestroyAPIView should return a single object when not excluded
422+
"""
423+
setattr(InstanceView, 'filter_backend', InclusiveFilterBackend)
424+
request = factory.get('/1')
425+
response = self.instance_view(request, pk=1).render()
426+
self.assertEqual(response.status_code, status.HTTP_200_OK)
427+
self.assertEqual(response.data, {'id': 1, 'text': 'foo'})

0 commit comments

Comments
 (0)
0