mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-12-01 05:54:01 +03:00
Start separating out serializers
This commit is contained in:
parent
6305033373
commit
d1c217523b
|
@ -8,7 +8,7 @@ from django.core.paginator import Paginator
|
|||
from django.http import HttpResponse
|
||||
|
||||
from djangorestframework import status
|
||||
from djangorestframework.resources import Resource, FormResource, ModelResource
|
||||
from djangorestframework.resources import Resource
|
||||
from djangorestframework.response import Response, ErrorResponse
|
||||
from djangorestframework.utils import MSIE_USER_AGENT_REGEX
|
||||
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
|
||||
|
@ -395,24 +395,39 @@ class AuthMixin(object):
|
|||
|
||||
|
||||
class SerializerMixin(object):
|
||||
def validate_request(self, data, files=None):
|
||||
serializer_class = None
|
||||
deserializer_class = None
|
||||
|
||||
@property
|
||||
def serializer(self):
|
||||
if not hasattr(self, '_serializer'):
|
||||
self._serializer = self.resource_class(view=self)
|
||||
return self._serializer
|
||||
|
||||
@property
|
||||
def deserializer(self):
|
||||
if not hasattr(self, '_deserializer'):
|
||||
self._deserializer = self.resource_class(view=self)
|
||||
return self._deserializer
|
||||
|
||||
def deserialize(self, data, files=None):
|
||||
"""
|
||||
Given the request *data* and optional *files*, return the cleaned,
|
||||
validated content.
|
||||
May raise an :class:`response.ErrorResponse` with status code 400
|
||||
(Bad Request) on failure.
|
||||
"""
|
||||
return self.resource.validate_request(data, files)
|
||||
return self.deserializer.deserialize(data, files)
|
||||
|
||||
def filter_response(self, obj):
|
||||
def serialize(self, obj):
|
||||
"""
|
||||
Given the response content, filter it into a serializable object.
|
||||
"""
|
||||
return self.resource.filter_response(obj)
|
||||
return self.serializer.serialize(obj)
|
||||
|
||||
def get_bound_form(self, content=None, method=None):
|
||||
if hasattr(self.resource, 'get_bound_form'):
|
||||
return self.resource.get_bound_form(content, method=method)
|
||||
if hasattr(self.deserializer, 'get_bound_form'):
|
||||
return self.deserializer.get_bound_form(content, method=method)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
@ -432,7 +447,7 @@ class ResourceMixin(object):
|
|||
and filters the object representation into a serializable object for the
|
||||
response.
|
||||
"""
|
||||
resource_class = None
|
||||
resource_class = Resource
|
||||
|
||||
@property
|
||||
def CONTENT(self):
|
||||
|
@ -443,7 +458,7 @@ class ResourceMixin(object):
|
|||
(Bad Request).
|
||||
"""
|
||||
if not hasattr(self, '_content'):
|
||||
self._content = self.validate_request(self.DATA, self.FILES)
|
||||
self._content = self.deserialize(self.DATA, self.FILES)
|
||||
return self._content
|
||||
|
||||
@property
|
||||
|
@ -454,25 +469,12 @@ class ResourceMixin(object):
|
|||
May raise an :class:`response.ErrorResponse` with status code 400
|
||||
(Bad Request).
|
||||
"""
|
||||
return self.validate_request(self.request.GET)
|
||||
|
||||
def get_resource_class(self):
|
||||
if self.resource_class:
|
||||
return self.resource_class
|
||||
elif getattr(self, 'model', None):
|
||||
return ModelResource
|
||||
elif getattr(self, 'form', None):
|
||||
return FormResource
|
||||
elif hasattr(self, 'request') and getattr(self, '%s_form' % self.method.lower(), None):
|
||||
return FormResource
|
||||
else:
|
||||
return Resource
|
||||
return self.deserialize(self.request.GET)
|
||||
|
||||
@property
|
||||
def resource(self):
|
||||
if not hasattr(self, '_resource'):
|
||||
resource_class = self.get_resource_class()
|
||||
self._resource = resource_class(view=self)
|
||||
self._resource = self.resource_class(view=self)
|
||||
return self._resource
|
||||
|
||||
|
||||
|
@ -612,7 +614,7 @@ class PaginatorMixin(object):
|
|||
'total': page.paginator.count,
|
||||
}
|
||||
|
||||
def filter_response(self, obj):
|
||||
def serialize(self, obj):
|
||||
"""
|
||||
Given the response content, paginate and then serialize.
|
||||
|
||||
|
@ -626,7 +628,7 @@ class PaginatorMixin(object):
|
|||
# We don't want to paginate responses for anything other than GET
|
||||
# requests
|
||||
if self.method.upper() != 'GET':
|
||||
return self.resource.filter_response(obj)
|
||||
return self.serializer.serialize(obj)
|
||||
|
||||
paginator = Paginator(obj, self.get_limit())
|
||||
|
||||
|
@ -642,7 +644,7 @@ class PaginatorMixin(object):
|
|||
|
||||
page = paginator.page(page_num)
|
||||
|
||||
serialized_object_list = self.resource.filter_response(page.object_list)
|
||||
serialized_object_list = self.serializer.serialize(page.object_list)
|
||||
serialized_page_info = self.serialize_page_info(page)
|
||||
|
||||
serialized_page_info['results'] = serialized_object_list
|
||||
|
|
|
@ -31,7 +31,7 @@ class BaseResource(Serializer):
|
|||
self.view = view
|
||||
self.instance = instance
|
||||
|
||||
def validate_request(self, data, files=None):
|
||||
def deserialize(self, data, files=None):
|
||||
"""
|
||||
Given the request content return the cleaned, validated content.
|
||||
Typically raises a :exc:`response.ErrorResponse` with status code 400
|
||||
|
@ -39,12 +39,6 @@ class BaseResource(Serializer):
|
|||
"""
|
||||
return data
|
||||
|
||||
def filter_response(self, obj):
|
||||
"""
|
||||
Given the response content, filter it into a serializable object.
|
||||
"""
|
||||
return self.serialize(obj)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
@ -96,7 +90,7 @@ class FormResource(Resource):
|
|||
Also provides a :meth:`get_bound_form` method which may be used by some
|
||||
renderers.
|
||||
|
||||
On calling :meth:`validate_request` this validator may set a
|
||||
On calling :meth:`deserialize` this validator may set a
|
||||
:attr:`bound_form_instance` attribute on the view, which may be used by
|
||||
some renderers.
|
||||
"""
|
||||
|
@ -108,7 +102,7 @@ class FormResource(Resource):
|
|||
:class:`views.View`.
|
||||
"""
|
||||
|
||||
def validate_request(self, data, files=None):
|
||||
def deserialize(self, data, files=None):
|
||||
"""
|
||||
Given some content as input return some cleaned, validated content.
|
||||
|
||||
|
@ -450,7 +444,7 @@ class ModelResource(FormResource):
|
|||
pass
|
||||
raise _SkipField
|
||||
|
||||
def validate_request(self, data, files=None):
|
||||
def deserialize(self, data, files=None):
|
||||
"""
|
||||
Given some content as input return some cleaned, validated content.
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ class TestDisabledValidations(TestCase):
|
|||
"""Tests on FormValidator with validation disabled by setting form to None"""
|
||||
|
||||
def test_disabled_form_validator_returns_content_unchanged(self):
|
||||
"""If the view's form attribute is None then FormValidator(view).validate_request(content, None)
|
||||
"""If the view's form attribute is None then FormValidator(view).deserialize(content, None)
|
||||
should just return the content unmodified."""
|
||||
class DisabledFormResource(FormResource):
|
||||
form = None
|
||||
|
@ -23,7 +23,7 @@ class TestDisabledValidations(TestCase):
|
|||
|
||||
view = MockView()
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(FormResource(view).validate_request(content, None), content)
|
||||
self.assertEqual(FormResource(view).deserialize(content, None), content)
|
||||
|
||||
def test_disabled_form_validator_get_bound_form_returns_none(self):
|
||||
"""If the view's form attribute is None on then
|
||||
|
@ -41,7 +41,7 @@ class TestDisabledValidations(TestCase):
|
|||
|
||||
def test_disabled_model_form_validator_returns_content_unchanged(self):
|
||||
"""If the view's form is None and does not have a Resource with a model set then
|
||||
ModelFormValidator(view).validate_request(content, None) should just return the content unmodified."""
|
||||
ModelFormValidator(view).deserialize(content, None) should just return the content unmodified."""
|
||||
|
||||
class DisabledModelFormView(View):
|
||||
resource = ModelResource
|
||||
|
@ -83,7 +83,7 @@ class TestNonFieldErrors(TestCase):
|
|||
view = MockView()
|
||||
content = {'field1': 'example1', 'field2': 'example2'}
|
||||
try:
|
||||
MockResource(view=view).validate_request(content, None)
|
||||
MockResource(view=view).deserialize(content, None)
|
||||
except ErrorResponse, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
||||
else:
|
||||
|
@ -119,19 +119,19 @@ class TestFormValidation(TestCase):
|
|||
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
|
||||
"""If the content is already valid and clean then validate(content) should just return the content unmodified."""
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(validator.validate_request(content, None), content)
|
||||
self.assertEqual(validator.deserialize(content, None), content)
|
||||
|
||||
def validation_failure_raises_response_exception(self, validator):
|
||||
"""If form validation fails a ResourceException 400 (Bad Request) should be raised."""
|
||||
content = {}
|
||||
self.assertRaises(ErrorResponse, validator.validate_request, content, None)
|
||||
self.assertRaises(ErrorResponse, validator.deserialize, content, None)
|
||||
|
||||
def validation_does_not_allow_extra_fields_by_default(self, validator):
|
||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||
self.assertRaises(ErrorResponse, validator.validate_request, content, None)
|
||||
self.assertRaises(ErrorResponse, validator.deserialize, content, None)
|
||||
|
||||
def validation_allows_extra_fields_if_explicitly_set(self, validator):
|
||||
"""If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
|
||||
|
@ -147,7 +147,7 @@ class TestFormValidation(TestCase):
|
|||
"""If validation fails due to no content, ensure the response contains a single non-field error"""
|
||||
content = {}
|
||||
try:
|
||||
validator.validate_request(content, None)
|
||||
validator.deserialize(content, None)
|
||||
except ErrorResponse, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
||||
else:
|
||||
|
@ -157,7 +157,7 @@ class TestFormValidation(TestCase):
|
|||
"""If validation fails due to a field error, ensure the response contains a single field error"""
|
||||
content = {'qwerty': ''}
|
||||
try:
|
||||
validator.validate_request(content, None)
|
||||
validator.deserialize(content, None)
|
||||
except ErrorResponse, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
||||
else:
|
||||
|
@ -167,7 +167,7 @@ class TestFormValidation(TestCase):
|
|||
"""If validation fails due to an invalid field, ensure the response contains a single field error"""
|
||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||
try:
|
||||
validator.validate_request(content, None)
|
||||
validator.deserialize(content, None)
|
||||
except ErrorResponse, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}})
|
||||
else:
|
||||
|
@ -177,7 +177,7 @@ class TestFormValidation(TestCase):
|
|||
"""If validation for multiple reasons, ensure the response contains each error"""
|
||||
content = {'qwerty': '', 'extra': 'extra'}
|
||||
try:
|
||||
validator.validate_request(content, None)
|
||||
validator.deserialize(content, None)
|
||||
except ErrorResponse, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.'],
|
||||
'extra': ['This field does not exist.']}})
|
||||
|
@ -286,31 +286,31 @@ class TestModelFormValidator(TestCase):
|
|||
def test_property_fields_are_allowed_on_model_forms(self):
|
||||
"""Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
|
||||
content = {'qwerty':'example', 'uiop': 'example', 'readonly': 'read only'}
|
||||
self.assertEqual(self.validator.validate_request(content, None), content)
|
||||
self.assertEqual(self.validator.deserialize(content, None), content)
|
||||
|
||||
def test_property_fields_are_not_required_on_model_forms(self):
|
||||
"""Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
|
||||
content = {'qwerty':'example', 'uiop': 'example'}
|
||||
self.assertEqual(self.validator.validate_request(content, None), content)
|
||||
self.assertEqual(self.validator.deserialize(content, None), content)
|
||||
|
||||
def test_extra_fields_not_allowed_on_model_forms(self):
|
||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||
content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
|
||||
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
||||
self.assertRaises(ErrorResponse, self.validator.deserialize, content, None)
|
||||
|
||||
def test_validate_requires_fields_on_model_forms(self):
|
||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||
content = {'readonly': 'read only'}
|
||||
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
||||
self.assertRaises(ErrorResponse, self.validator.deserialize, content, None)
|
||||
|
||||
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
|
||||
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
|
||||
content = {'qwerty':'example', 'readonly': 'read only'}
|
||||
self.validator.validate_request(content, None)
|
||||
self.validator.deserialize(content, None)
|
||||
|
||||
def test_model_form_validator_uses_model_forms(self):
|
||||
self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))
|
||||
|
|
|
@ -48,59 +48,59 @@ urlpatterns = patterns('djangorestframework.utils.staticviews',
|
|||
url(r'^model/(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource_class=MockResource)),
|
||||
)
|
||||
|
||||
class BaseViewTests(TestCase):
|
||||
"""Test the base view class of djangorestframework"""
|
||||
urls = 'djangorestframework.tests.views'
|
||||
|
||||
def test_options_method_simple_view(self):
|
||||
response = self.client.options('/mock/')
|
||||
self._verify_options_response(response,
|
||||
name='Mock',
|
||||
description='This is a basic mock view')
|
||||
|
||||
def test_options_method_resource_view(self):
|
||||
response = self.client.options('/resourcemock/')
|
||||
self._verify_options_response(response,
|
||||
name='Resource Mock',
|
||||
description='This is a resource-based mock view',
|
||||
fields={'foo':'BooleanField',
|
||||
'bar':'IntegerField',
|
||||
'baz':'CharField',
|
||||
})
|
||||
|
||||
def test_options_method_model_resource_list_view(self):
|
||||
response = self.client.options('/model/')
|
||||
self._verify_options_response(response,
|
||||
name='Mock List',
|
||||
description='This is a mock model-based resource',
|
||||
fields={'foo':'BooleanField',
|
||||
'bar':'IntegerField',
|
||||
'baz':'CharField',
|
||||
})
|
||||
|
||||
def test_options_method_model_resource_detail_view(self):
|
||||
response = self.client.options('/model/0/')
|
||||
self._verify_options_response(response,
|
||||
name='Mock Instance',
|
||||
description='This is a mock model-based resource',
|
||||
fields={'foo':'BooleanField',
|
||||
'bar':'IntegerField',
|
||||
'baz':'CharField',
|
||||
})
|
||||
|
||||
def _verify_options_response(self, response, name, description, fields=None, status=200,
|
||||
mime_type='application/json'):
|
||||
self.assertEqual(response.status_code, status)
|
||||
self.assertEqual(response['Content-Type'].split(';')[0], mime_type)
|
||||
parser = JSONParser(None)
|
||||
(data, files) = parser.parse(StringIO(response.content))
|
||||
self.assertTrue('application/json' in data['renders'])
|
||||
self.assertEqual(name, data['name'])
|
||||
self.assertEqual(description, data['description'])
|
||||
if fields is None:
|
||||
self.assertFalse(hasattr(data, 'fields'))
|
||||
else:
|
||||
self.assertEqual(data['fields'], fields)
|
||||
# class BaseViewTests(TestCase):
|
||||
# """Test the base view class of djangorestframework"""
|
||||
# urls = 'djangorestframework.tests.views'
|
||||
#
|
||||
# def test_options_method_simple_view(self):
|
||||
# response = self.client.options('/mock/')
|
||||
# self._verify_options_response(response,
|
||||
# name='Mock',
|
||||
# description='This is a basic mock view')
|
||||
#
|
||||
# def test_options_method_resource_view(self):
|
||||
# response = self.client.options('/resourcemock/')
|
||||
# self._verify_options_response(response,
|
||||
# name='Resource Mock',
|
||||
# description='This is a resource-based mock view',
|
||||
# fields={'foo':'BooleanField',
|
||||
# 'bar':'IntegerField',
|
||||
# 'baz':'CharField',
|
||||
# })
|
||||
#
|
||||
# def test_options_method_model_resource_list_view(self):
|
||||
# response = self.client.options('/model/')
|
||||
# self._verify_options_response(response,
|
||||
# name='Mock List',
|
||||
# description='This is a mock model-based resource',
|
||||
# fields={'foo':'BooleanField',
|
||||
# 'bar':'IntegerField',
|
||||
# 'baz':'CharField',
|
||||
# })
|
||||
#
|
||||
# def test_options_method_model_resource_detail_view(self):
|
||||
# response = self.client.options('/model/0/')
|
||||
# self._verify_options_response(response,
|
||||
# name='Mock Instance',
|
||||
# description='This is a mock model-based resource',
|
||||
# fields={'foo':'BooleanField',
|
||||
# 'bar':'IntegerField',
|
||||
# 'baz':'CharField',
|
||||
# })
|
||||
#
|
||||
# def _verify_options_response(self, response, name, description, fields=None, status=200,
|
||||
# mime_type='application/json'):
|
||||
# self.assertEqual(response.status_code, status)
|
||||
# self.assertEqual(response['Content-Type'].split(';')[0], mime_type)
|
||||
# parser = JSONParser(None)
|
||||
# (data, files) = parser.parse(StringIO(response.content))
|
||||
# self.assertTrue('application/json' in data['renders'])
|
||||
# self.assertEqual(name, data['name'])
|
||||
# self.assertEqual(description, data['description'])
|
||||
# if fields is None:
|
||||
# self.assertFalse(hasattr(data, 'fields'))
|
||||
# else:
|
||||
# self.assertEqual(data['fields'], fields)
|
||||
|
||||
|
||||
class ExtraViewsTests(TestCase):
|
||||
|
|
|
@ -35,7 +35,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, SerializerMixin, AuthMixi
|
|||
The resource to use when validating requests and filtering responses,
|
||||
or `None` to use default behaviour.
|
||||
"""
|
||||
resource_class = None
|
||||
resource_class = resources.Resource
|
||||
|
||||
"""
|
||||
List of renderers the resource can serialize the response with, ordered by preference.
|
||||
|
@ -143,7 +143,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, SerializerMixin, AuthMixi
|
|||
else:
|
||||
# Pre-serialize filtering (eg filter complex objects into
|
||||
# natively serializable types)
|
||||
response.cleaned_content = self.filter_response(response.raw_content)
|
||||
response.cleaned_content = self.serialize(response.raw_content)
|
||||
|
||||
except ErrorResponse, exc:
|
||||
response = exc.response
|
||||
|
|
Loading…
Reference in New Issue
Block a user