mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-24 08:14:16 +03:00
Merge branch 'patch'
This commit is contained in:
commit
438c2cac1b
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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.
|
||||||
|
|
27
rest_framework/tests/utils.py
Normal file
27
rest_framework/tests/utils.py
Normal 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)
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user