Merge branch 'patch'

This commit is contained in:
Tom Christie 2013-01-02 13:47:58 +00:00
commit 438c2cac1b
9 changed files with 120 additions and 15 deletions

View File

@ -85,7 +85,7 @@ Extends: [SingleObjectAPIView], [DestroyModelMixin]
Used for **update-only** endpoints for a **single model instance**. Used for **update-only** endpoints for a **single model instance**.
Provides a `put` method handler. Provides `put` and `patch` method handlers.
Extends: [SingleObjectAPIView], [UpdateModelMixin] Extends: [SingleObjectAPIView], [UpdateModelMixin]
@ -97,6 +97,14 @@ Provides `get` and `post` method handlers.
Extends: [MultipleObjectAPIView], [ListModelMixin], [CreateModelMixin] 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 ## RetrieveDestroyAPIView
Used for **read or delete** endpoints to represent a **single model instance**. 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**. 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] 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. 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]. Should be mixed in with [SingleObjectAPIView].
## DestroyModelMixin ## DestroyModelMixin

View File

@ -18,7 +18,9 @@ Major version numbers (x.0.0) are reserved for project milestones. No major poi
### Master ### 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 ### 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: Ensure read-only fields don't have model validation applied.
* Bugfix: Fix hyperlinked fields in paginated results. * Bugfix: Fix hyperlinked fields in paginated results.
### 2.1.9 ## 2.1.9
**Date**: 11th Dec 2012 **Date**: 11th Dec 2012

View File

@ -96,6 +96,12 @@ else:
update_wrapper(view, cls.dispatch, assigned=()) update_wrapper(view, cls.dispatch, assigned=())
return view 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. # PUT, DELETE do not require CSRF until 1.4. They should. Make it better.
if django.VERSION >= (1, 4): if django.VERSION >= (1, 4):
from django.middleware.csrf import CsrfViewMiddleware from django.middleware.csrf import CsrfViewMiddleware

View File

@ -47,14 +47,16 @@ class GenericAPIView(views.APIView):
return serializer_class 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 Return the serializer instance that should be used for validating and
deserializing input, and for serializing output. deserializing input, and for serializing output.
""" """
serializer_class = self.get_serializer_class() serializer_class = self.get_serializer_class()
context = self.get_serializer_context() 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): class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
@ -171,6 +173,10 @@ class UpdateAPIView(mixins.UpdateModelMixin,
def put(self, request, *args, **kwargs): def put(self, request, *args, **kwargs):
return self.update(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, class ListCreateAPIView(mixins.ListModelMixin,
mixins.CreateModelMixin, mixins.CreateModelMixin,
@ -185,6 +191,19 @@ class ListCreateAPIView(mixins.ListModelMixin,
return self.create(request, *args, **kwargs) 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, class RetrieveDestroyAPIView(mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
SingleObjectAPIView): SingleObjectAPIView):
@ -213,3 +232,7 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs) return self.destroy(request, *args, **kwargs)
def patch(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)

View File

@ -16,11 +16,14 @@ class CreateModelMixin(object):
""" """
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA, files=request.FILES) serializer = self.get_serializer(data=request.DATA, files=request.FILES)
if serializer.is_valid(): if serializer.is_valid():
self.pre_save(serializer.object) self.pre_save(serializer.object)
self.object = serializer.save() self.object = serializer.save()
headers = self.get_success_headers(serializer.data) 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) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def get_success_headers(self, data): def get_success_headers(self, data):
@ -82,20 +85,21 @@ class UpdateModelMixin(object):
Should be mixed in with `SingleObjectBaseView`. Should be mixed in with `SingleObjectBaseView`.
""" """
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
try: try:
self.object = self.get_object() self.object = self.get_object()
created = False success_status_code = status.HTTP_200_OK
except Http404: except Http404:
self.object = None 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(): if serializer.is_valid():
self.pre_save(serializer.object) self.pre_save(serializer.object)
self.object = serializer.save() self.object = serializer.save()
status_code = created and status.HTTP_201_CREATED or status.HTTP_200_OK return Response(serializer.data, status=success_status_code)
return Response(serializer.data, status=status_code)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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 # 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. # 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): class DestroyModelMixin(object):

View File

@ -17,6 +17,8 @@ from rest_framework.decorators import (
permission_classes, permission_classes,
) )
from rest_framework.tests.utils import RequestFactory
class DecoratorTestCase(TestCase): class DecoratorTestCase(TestCase):
@ -63,6 +65,20 @@ class DecoratorTestCase(TestCase):
response = view(request) response = view(request)
self.assertEqual(response.status_code, 405) 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): def test_renderer_classes(self):
@api_view(['GET']) @api_view(['GET'])

View File

@ -1,8 +1,8 @@
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import simplejson as json from django.utils import simplejson as json
from rest_framework import generics, serializers, status from rest_framework import generics, serializers, status
from rest_framework.tests.utils import RequestFactory
from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
@ -181,6 +181,20 @@ class TestInstanceView(TestCase):
updated = self.objects.get(id=1) updated = self.objects.get(id=1)
self.assertEquals(updated.text, 'foobar') 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): def test_delete_instance_view(self):
""" """
DELETE requests to RetrieveUpdateDestroyAPIView should delete an object. DELETE requests to RetrieveUpdateDestroyAPIView should delete an object.

View File

@ -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)

View File

@ -18,7 +18,7 @@ class BasicView(APIView):
return Response({'method': 'POST', 'data': request.DATA}) return Response({'method': 'POST', 'data': request.DATA})
@api_view(['GET', 'POST', 'PUT']) @api_view(['GET', 'POST', 'PUT', 'PATCH'])
def basic_view(request): def basic_view(request):
if request.method == 'GET': if request.method == 'GET':
return {'method': 'GET'} return {'method': 'GET'}
@ -26,6 +26,8 @@ def basic_view(request):
return {'method': 'POST', 'data': request.DATA} return {'method': 'POST', 'data': request.DATA}
elif request.method == 'PUT': elif request.method == 'PUT':
return {'method': 'PUT', 'data': request.DATA} return {'method': 'PUT', 'data': request.DATA}
elif request.method == 'PATCH':
return {'method': 'PATCH', 'data': request.DATA}
def sanitise_json_error(error_dict): def sanitise_json_error(error_dict):