diff options
| author | Tom Christie | 2013-01-02 13:47:58 +0000 | 
|---|---|---|
| committer | Tom Christie | 2013-01-02 13:47:58 +0000 | 
| commit | 438c2cac1bcd5ac24ea83bc75deb12b8584b906a (patch) | |
| tree | ebd6727f2d95813f3e6efd4de07a7027e236ef31 | |
| parent | d379997aba5b1e41309bbed8740ed704c0feb58b (diff) | |
| parent | ef73160599ef836f47801fe550168ecdaa3e20d6 (diff) | |
| download | django-rest-framework-438c2cac1bcd5ac24ea83bc75deb12b8584b906a.tar.bz2 | |
Merge branch 'patch'
| -rw-r--r-- | docs/api-guide/generic-views.md | 14 | ||||
| -rw-r--r-- | docs/topics/release-notes.md | 6 | ||||
| -rw-r--r-- | rest_framework/compat.py | 6 | ||||
| -rw-r--r-- | rest_framework/generics.py | 27 | ||||
| -rw-r--r-- | rest_framework/mixins.py | 19 | ||||
| -rw-r--r-- | rest_framework/tests/decorators.py | 16 | ||||
| -rw-r--r-- | rest_framework/tests/generics.py | 16 | ||||
| -rw-r--r-- | rest_framework/tests/utils.py | 27 | ||||
| -rw-r--r-- | rest_framework/tests/views.py | 4 | 
9 files changed, 120 insertions, 15 deletions
| diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 27c7d3f6..693e210d 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -85,7 +85,7 @@ Extends: [SingleObjectAPIView], [DestroyModelMixin]  Used for **update-only** endpoints for a **single model instance**. -Provides a `put` method handler. +Provides `put` and `patch` method handlers.  Extends: [SingleObjectAPIView], [UpdateModelMixin] @@ -97,6 +97,14 @@ Provides `get` and `post` method handlers.  Extends: [MultipleObjectAPIView], [ListModelMixin], [CreateModelMixin] +## RetrieveUpdateAPIView + +Used for **read or update** endpoints to represent a **single model instance**. + +Provides `get`, `put` and `patch` method handlers. + +Extends: [SingleObjectAPIView], [RetrieveModelMixin], [UpdateModelMixin] +  ## RetrieveDestroyAPIView  Used for **read or delete** endpoints to represent a **single model instance**. @@ -109,7 +117,7 @@ Extends: [SingleObjectAPIView], [RetrieveModelMixin], [DestroyModelMixin]  Used for **read-write-delete** endpoints to represent a **single model instance**. -Provides `get`, `put` and `delete` method handlers. +Provides `get`, `put`, `patch` and `delete` method handlers.  Extends: [SingleObjectAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyModelMixin] @@ -197,6 +205,8 @@ If an object is created, for example when making a `DELETE` request followed by  If the request data provided for updating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response. +A boolean `partial` keyword argument may be supplied to the `.update()` method.  If `partial` is set to `True`, all fields for the update will be optional.  This allows support for HTTP `PATCH` requests. +  Should be mixed in with [SingleObjectAPIView].  ## DestroyModelMixin diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 5b34bf3d..e4bd1217 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -18,7 +18,9 @@ Major version numbers (x.0.0) are reserved for project milestones.  No major poi  ### Master -* Relation changes are no longer persisted in `.restore_object` +* Added `PATCH` support. +* Added `RetrieveUpdateAPIView`. +* Relation changes are now persisted in `save` instead of in `.restore_object`.  ### 2.1.14 @@ -61,7 +63,7 @@ This change will not affect user code, so long as it's following the recommended  * Bugfix: Ensure read-only fields don't have model validation applied.  * Bugfix: Fix hyperlinked fields in paginated results. -### 2.1.9 +## 2.1.9  **Date**: 11th Dec 2012 diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 86952fb8..5508f6c0 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -96,6 +96,12 @@ else:              update_wrapper(view, cls.dispatch, assigned=())              return view +# Taken from @markotibold's attempt at supporting PATCH. +# https://github.com/markotibold/django-rest-framework/tree/patch +http_method_names = set(View.http_method_names) +http_method_names.add('patch') +View.http_method_names = list(http_method_names)  # PATCH method is not implemented by Django +  # PUT, DELETE do not require CSRF until 1.4.  They should.  Make it better.  if django.VERSION >= (1, 4):      from django.middleware.csrf import CsrfViewMiddleware diff --git a/rest_framework/generics.py b/rest_framework/generics.py index dd8dfcf8..cda9ca79 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -47,14 +47,16 @@ class GenericAPIView(views.APIView):          return serializer_class -    def get_serializer(self, instance=None, data=None, files=None): +    def get_serializer(self, instance=None, data=None, +                       files=None, partial=False):          """          Return the serializer instance that should be used for validating and          deserializing input, and for serializing output.          """          serializer_class = self.get_serializer_class()          context = self.get_serializer_context() -        return serializer_class(instance, data=data, files=files, context=context) +        return serializer_class(instance, data=data, files=files, +                                partial=partial, context=context)  class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): @@ -171,6 +173,10 @@ class UpdateAPIView(mixins.UpdateModelMixin,      def put(self, request, *args, **kwargs):          return self.update(request, *args, **kwargs) +    def patch(self, request, *args, **kwargs): +        kwargs['partial'] = True +        return self.update(request, *args, **kwargs) +  class ListCreateAPIView(mixins.ListModelMixin,                          mixins.CreateModelMixin, @@ -185,6 +191,19 @@ class ListCreateAPIView(mixins.ListModelMixin,          return self.create(request, *args, **kwargs) +class RetrieveUpdateAPIView(mixins.RetrieveModelMixin, +                            mixins.UpdateModelMixin, +                            SingleObjectAPIView): +    """ +    Concrete view for retrieving, updating a model instance. +    """ +    def get(self, request, *args, **kwargs): +        return self.retrieve(request, *args, **kwargs) + +    def put(self, request, *args, **kwargs): +        return self.update(request, *args, **kwargs) + +  class RetrieveDestroyAPIView(mixins.RetrieveModelMixin,                               mixins.DestroyModelMixin,                               SingleObjectAPIView): @@ -213,3 +232,7 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,      def delete(self, request, *args, **kwargs):          return self.destroy(request, *args, **kwargs) + +    def patch(self, request, *args, **kwargs): +        kwargs['partial'] = True +        return self.update(request, *args, **kwargs) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 2700606d..43581ae9 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -16,11 +16,14 @@ class CreateModelMixin(object):      """      def create(self, request, *args, **kwargs):          serializer = self.get_serializer(data=request.DATA, files=request.FILES) +          if serializer.is_valid():              self.pre_save(serializer.object)              self.object = serializer.save()              headers = self.get_success_headers(serializer.data) -            return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) +            return Response(serializer.data, status=status.HTTP_201_CREATED, +                            headers=headers) +          return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)      def get_success_headers(self, data): @@ -82,20 +85,21 @@ class UpdateModelMixin(object):      Should be mixed in with `SingleObjectBaseView`.      """      def update(self, request, *args, **kwargs): +        partial = kwargs.pop('partial', False)          try:              self.object = self.get_object() -            created = False +            success_status_code = status.HTTP_200_OK          except Http404:              self.object = None -            created = True +            success_status_code = status.HTTP_201_CREATED -        serializer = self.get_serializer(self.object, data=request.DATA, files=request.FILES) +        serializer = self.get_serializer(self.object, data=request.DATA, +                                         files=request.FILES, partial=partial)          if serializer.is_valid():              self.pre_save(serializer.object)              self.object = serializer.save() -            status_code = created and status.HTTP_201_CREATED or status.HTTP_200_OK -            return Response(serializer.data, status=status_code) +            return Response(serializer.data, status=success_status_code)          return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -115,7 +119,8 @@ class UpdateModelMixin(object):          # Ensure we clean the attributes so that we don't eg return integer          # pk using a string representation, as provided by the url conf kwarg. -        obj.full_clean() +        if hasattr(obj, 'full_clean'): +            obj.full_clean()  class DestroyModelMixin(object): diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py index 8079c8cb..bc44a45b 100644 --- a/rest_framework/tests/decorators.py +++ b/rest_framework/tests/decorators.py @@ -17,6 +17,8 @@ from rest_framework.decorators import (      permission_classes,  ) +from rest_framework.tests.utils import RequestFactory +  class DecoratorTestCase(TestCase): @@ -63,6 +65,20 @@ class DecoratorTestCase(TestCase):          response = view(request)          self.assertEqual(response.status_code, 405) +    def test_calling_patch_method(self): + +        @api_view(['GET', 'PATCH']) +        def view(request): +            return Response({}) + +        request = self.factory.patch('/') +        response = view(request) +        self.assertEqual(response.status_code, 200) + +        request = self.factory.post('/') +        response = view(request) +        self.assertEqual(response.status_code, 405) +      def test_renderer_classes(self):          @api_view(['GET']) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 7c24d84e..843017eb 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -1,8 +1,8 @@  from django.db import models  from django.test import TestCase -from django.test.client import RequestFactory  from django.utils import simplejson as json  from rest_framework import generics, serializers, status +from rest_framework.tests.utils import RequestFactory  from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel @@ -181,6 +181,20 @@ class TestInstanceView(TestCase):          updated = self.objects.get(id=1)          self.assertEquals(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') + +        response = self.view(request, pk=1).render() +        self.assertEquals(response.status_code, status.HTTP_200_OK) +        self.assertEquals(response.data, {'id': 1, 'text': 'foobar'}) +        updated = self.objects.get(id=1) +        self.assertEquals(updated.text, 'foobar') +      def test_delete_instance_view(self):          """          DELETE requests to RetrieveUpdateDestroyAPIView should delete an object. diff --git a/rest_framework/tests/utils.py b/rest_framework/tests/utils.py new file mode 100644 index 00000000..3906adb9 --- /dev/null +++ b/rest_framework/tests/utils.py @@ -0,0 +1,27 @@ +from django.test.client import RequestFactory, FakePayload +from django.test.client import MULTIPART_CONTENT +from urlparse import urlparse + + +class RequestFactory(RequestFactory): + +    def __init__(self, **defaults): +        super(RequestFactory, self).__init__(**defaults) + +    def patch(self, path, data={}, content_type=MULTIPART_CONTENT, +            **extra): +        "Construct a PATCH request." + +        patch_data = self._encode_data(data, content_type) + +        parsed = urlparse(path) +        r = { +            'CONTENT_LENGTH': len(patch_data), +            'CONTENT_TYPE':   content_type, +            'PATH_INFO':      self._get_path(parsed), +            'QUERY_STRING':   parsed[4], +            'REQUEST_METHOD': 'PATCH', +            'wsgi.input':     FakePayload(patch_data), +        } +        r.update(extra) +        return self.request(**r) diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py index 43365e07..7cd82656 100644 --- a/rest_framework/tests/views.py +++ b/rest_framework/tests/views.py @@ -18,7 +18,7 @@ class BasicView(APIView):          return Response({'method': 'POST', 'data': request.DATA}) -@api_view(['GET', 'POST', 'PUT']) +@api_view(['GET', 'POST', 'PUT', 'PATCH'])  def basic_view(request):      if request.method == 'GET':          return {'method': 'GET'} @@ -26,6 +26,8 @@ def basic_view(request):          return {'method': 'POST', 'data': request.DATA}      elif request.method == 'PUT':          return {'method': 'PUT', 'data': request.DATA} +    elif request.method == 'PATCH': +        return {'method': 'PATCH', 'data': request.DATA}  def sanitise_json_error(error_dict): | 
