diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index f564890cc..b40b0102d 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -24,6 +24,28 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView): model = BasicModel +class InstanceDetailView(generics.RetrieveUpdateDestroyAPIView): + """ + Example detail view for override of get_object(). + """ + + # we have to implement this too, otherwise we can't be sure that get_object + # will be called + def get_serializer(self, instance=None, data=None, files=None, partial=None): + class InstanceDetailSerializer(serializers.ModelSerializer): + class Meta: + model = BasicModel + return InstanceDetailSerializer(instance=instance, data=data, files=files, partial=partial) + + def get_object(self): + try: + pk = int(self.kwargs['pk']) + self.object = BasicModel.objects.get(id=pk) + return self.object + except BasicModel.DoesNotExist: + return self.permission_denied(self.request) + + class SlugSerializer(serializers.ModelSerializer): slug = serializers.Field() # read only @@ -301,6 +323,157 @@ class TestInstanceView(TestCase): new_obj = SlugBasedModel.objects.get(slug='test_slug') self.assertEqual(new_obj.text, 'foobar') +class TestInstanceDetailView(TestCase): + """ + Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the + queryset/model mechanism but instead overrides get_object() + """ + def setUp(self): + """ + Create 3 BasicModel intances. + """ + items = ['foo', 'bar', 'baz'] + for item in items: + BasicModel(text=item).save() + self.objects = BasicModel.objects + self.data = [ + {'id': obj.id, 'text': obj.text} + for obj in self.objects.all() + ] + self.view_class = InstanceDetailView + self.view = InstanceDetailView.as_view() + + def test_get_instance_view(self): + """ + GET requests to RetrieveUpdateDestroyAPIView should return a single object. + """ + request = factory.get('/1') + with self.assertNumQueries(1): + response = self.view(request, pk=1).render() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, self.data[0]) + + def test_post_instance_view(self): + """ + POST requests to RetrieveUpdateDestroyAPIView should not be allowed + """ + content = {'text': 'foobar'} + request = factory.post('/', json.dumps(content), + content_type='application/json') + with self.assertNumQueries(0): + response = self.view(request).render() + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + self.assertEqual(response.data, {"detail": "Method 'POST' not allowed."}) + + def test_put_instance_view(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView should update an object. + """ + content = {'text': 'foobar'} + request = factory.put('/1', json.dumps(content), + content_type='application/json') + with self.assertNumQueries(2): + response = self.view(request, pk='1').render() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) + updated = self.objects.get(id=1) + self.assertEqual(updated.text, 'foobar') + + def test_patch_instance_view(self): + """ + PATCH requests to RetrieveUpdateDestroyAPIView should update an object. + """ + content = {'text': 'foobar'} + request = factory.patch('/1', json.dumps(content), + content_type='application/json') + + with self.assertNumQueries(2): + response = self.view(request, pk=1).render() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) + updated = self.objects.get(id=1) + self.assertEqual(updated.text, 'foobar') + + def test_delete_instance_view(self): + """ + DELETE requests to RetrieveUpdateDestroyAPIView should delete an object. + """ + request = factory.delete('/1') + with self.assertNumQueries(2): + response = self.view(request, pk=1).render() + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(response.content, six.b('')) + ids = [obj.id for obj in self.objects.all()] + self.assertEqual(ids, [2, 3]) + + def test_options_instance_view(self): + """ + OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata + """ + request = factory.options('/') + with self.assertNumQueries(0): + response = self.view(request).render() + expected = { + 'parses': [ + 'application/json', + 'application/x-www-form-urlencoded', + 'multipart/form-data' + ], + 'renders': [ + 'application/json', + 'text/html' + ], + 'name': 'Instance Detail', + 'description': 'Example detail view for override of get_object().' + } + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + def test_put_cannot_set_id(self): + """ + PUT requests to create a new object should not be able to set the id. + """ + content = {'id': 999, 'text': 'foobar'} + request = factory.put('/1', json.dumps(content), + content_type='application/json') + with self.assertNumQueries(2): + response = self.view(request, pk=1).render() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) + updated = self.objects.get(id=1) + self.assertEqual(updated.text, 'foobar') + + def test_put_to_deleted_instance(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView should create an object + if it does not currently exist. In our DetailView, however, + we cannot access any other id's than those that already exist. + See the InstanceView for the normal behaviour. + """ + self.objects.get(id=1).delete() + content = {'text': 'foobar'} + request = factory.put('/1', json.dumps(content), + content_type='application/json') + with self.assertNumQueries(1): + response = self.view(request, pk=5).render() + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_put_as_create_on_id_based_url(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView should create an object + at the requested url if it doesn't exist. In our DetailView, however, + we cannot access any other id's than those that already exist. + See the InstanceView for the normal behaviour. + """ + content = {'text': 'foobar'} + # pk fields can not be created on demand, only the database can set the pk for a new object + request = factory.put('/5', json.dumps(content), + content_type='application/json') + with self.assertNumQueries(1): + response = self.view(request, pk=5).render() + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Regression test for #285