Start separating out serializers

This commit is contained in:
Tom Christie 2012-01-06 13:26:21 +00:00
parent 6305033373
commit d1c217523b
5 changed files with 104 additions and 108 deletions

View File

@ -8,7 +8,7 @@ from django.core.paginator import Paginator
from django.http import HttpResponse from django.http import HttpResponse
from djangorestframework import status from djangorestframework import status
from djangorestframework.resources import Resource, FormResource, ModelResource from djangorestframework.resources import Resource
from djangorestframework.response import Response, ErrorResponse from djangorestframework.response import Response, ErrorResponse
from djangorestframework.utils import MSIE_USER_AGENT_REGEX from djangorestframework.utils import MSIE_USER_AGENT_REGEX
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
@ -395,24 +395,39 @@ class AuthMixin(object):
class SerializerMixin(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, Given the request *data* and optional *files*, return the cleaned,
validated content. validated content.
May raise an :class:`response.ErrorResponse` with status code 400 May raise an :class:`response.ErrorResponse` with status code 400
(Bad Request) on failure. (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. 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): def get_bound_form(self, content=None, method=None):
if hasattr(self.resource, 'get_bound_form'): if hasattr(self.deserializer, 'get_bound_form'):
return self.resource.get_bound_form(content, method=method) return self.deserializer.get_bound_form(content, method=method)
else: else:
return None return None
@ -432,7 +447,7 @@ class ResourceMixin(object):
and filters the object representation into a serializable object for the and filters the object representation into a serializable object for the
response. response.
""" """
resource_class = None resource_class = Resource
@property @property
def CONTENT(self): def CONTENT(self):
@ -443,7 +458,7 @@ class ResourceMixin(object):
(Bad Request). (Bad Request).
""" """
if not hasattr(self, '_content'): 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 return self._content
@property @property
@ -454,25 +469,12 @@ class ResourceMixin(object):
May raise an :class:`response.ErrorResponse` with status code 400 May raise an :class:`response.ErrorResponse` with status code 400
(Bad Request). (Bad Request).
""" """
return self.validate_request(self.request.GET) return self.deserialize(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
@property @property
def resource(self): def resource(self):
if not hasattr(self, '_resource'): if not hasattr(self, '_resource'):
resource_class = self.get_resource_class() self._resource = self.resource_class(view=self)
self._resource = resource_class(view=self)
return self._resource return self._resource
@ -612,7 +614,7 @@ class PaginatorMixin(object):
'total': page.paginator.count, 'total': page.paginator.count,
} }
def filter_response(self, obj): def serialize(self, obj):
""" """
Given the response content, paginate and then serialize. 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 # We don't want to paginate responses for anything other than GET
# requests # requests
if self.method.upper() != 'GET': if self.method.upper() != 'GET':
return self.resource.filter_response(obj) return self.serializer.serialize(obj)
paginator = Paginator(obj, self.get_limit()) paginator = Paginator(obj, self.get_limit())
@ -642,7 +644,7 @@ class PaginatorMixin(object):
page = paginator.page(page_num) 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 = self.serialize_page_info(page)
serialized_page_info['results'] = serialized_object_list serialized_page_info['results'] = serialized_object_list

View File

@ -31,7 +31,7 @@ class BaseResource(Serializer):
self.view = view self.view = view
self.instance = instance 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. Given the request content return the cleaned, validated content.
Typically raises a :exc:`response.ErrorResponse` with status code 400 Typically raises a :exc:`response.ErrorResponse` with status code 400
@ -39,12 +39,6 @@ class BaseResource(Serializer):
""" """
return data 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): def retrieve(self, request, *args, **kwargs):
raise NotImplementedError() raise NotImplementedError()
@ -96,7 +90,7 @@ class FormResource(Resource):
Also provides a :meth:`get_bound_form` method which may be used by some Also provides a :meth:`get_bound_form` method which may be used by some
renderers. 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 :attr:`bound_form_instance` attribute on the view, which may be used by
some renderers. some renderers.
""" """
@ -108,7 +102,7 @@ class FormResource(Resource):
:class:`views.View`. :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. Given some content as input return some cleaned, validated content.
@ -450,7 +444,7 @@ class ModelResource(FormResource):
pass pass
raise _SkipField 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. Given some content as input return some cleaned, validated content.

View File

@ -13,7 +13,7 @@ class TestDisabledValidations(TestCase):
"""Tests on FormValidator with validation disabled by setting form to None""" """Tests on FormValidator with validation disabled by setting form to None"""
def test_disabled_form_validator_returns_content_unchanged(self): 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.""" should just return the content unmodified."""
class DisabledFormResource(FormResource): class DisabledFormResource(FormResource):
form = None form = None
@ -23,7 +23,7 @@ class TestDisabledValidations(TestCase):
view = MockView() view = MockView()
content = {'qwerty':'uiop'} 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): def test_disabled_form_validator_get_bound_form_returns_none(self):
"""If the view's form attribute is None on then """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): 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 """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): class DisabledModelFormView(View):
resource = ModelResource resource = ModelResource
@ -83,7 +83,7 @@ class TestNonFieldErrors(TestCase):
view = MockView() view = MockView()
content = {'field1': 'example1', 'field2': 'example2'} content = {'field1': 'example1', 'field2': 'example2'}
try: try:
MockResource(view=view).validate_request(content, None) MockResource(view=view).deserialize(content, None)
except ErrorResponse, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]}) self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
else: else:
@ -119,19 +119,19 @@ class TestFormValidation(TestCase):
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator): 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.""" """If the content is already valid and clean then validate(content) should just return the content unmodified."""
content = {'qwerty':'uiop'} 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): def validation_failure_raises_response_exception(self, validator):
"""If form validation fails a ResourceException 400 (Bad Request) should be raised.""" """If form validation fails a ResourceException 400 (Bad Request) should be raised."""
content = {} 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): 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. """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 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)""" broken clients more easily (eg submitting content with a misnamed field)"""
content = {'qwerty': 'uiop', 'extra': 'extra'} 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): 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.""" """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""" """If validation fails due to no content, ensure the response contains a single non-field error"""
content = {} content = {}
try: try:
validator.validate_request(content, None) validator.deserialize(content, None)
except ErrorResponse, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}}) self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
else: else:
@ -157,7 +157,7 @@ class TestFormValidation(TestCase):
"""If validation fails due to a field error, ensure the response contains a single field error""" """If validation fails due to a field error, ensure the response contains a single field error"""
content = {'qwerty': ''} content = {'qwerty': ''}
try: try:
validator.validate_request(content, None) validator.deserialize(content, None)
except ErrorResponse, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}}) self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
else: else:
@ -167,7 +167,7 @@ class TestFormValidation(TestCase):
"""If validation fails due to an invalid field, ensure the response contains a single field error""" """If validation fails due to an invalid field, ensure the response contains a single field error"""
content = {'qwerty': 'uiop', 'extra': 'extra'} content = {'qwerty': 'uiop', 'extra': 'extra'}
try: try:
validator.validate_request(content, None) validator.deserialize(content, None)
except ErrorResponse, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}}) self.assertEqual(exc.response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}})
else: else:
@ -177,7 +177,7 @@ class TestFormValidation(TestCase):
"""If validation for multiple reasons, ensure the response contains each error""" """If validation for multiple reasons, ensure the response contains each error"""
content = {'qwerty': '', 'extra': 'extra'} content = {'qwerty': '', 'extra': 'extra'}
try: try:
validator.validate_request(content, None) validator.deserialize(content, None)
except ErrorResponse, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.'], self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.'],
'extra': ['This field does not exist.']}}) 'extra': ['This field does not exist.']}})
@ -286,31 +286,31 @@ class TestModelFormValidator(TestCase):
def test_property_fields_are_allowed_on_model_forms(self): 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.""" """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'} 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): 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.""" """Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
content = {'qwerty':'example', 'uiop': 'example'} 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): 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. """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 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)""" broken clients more easily (eg submitting content with a misnamed field)"""
content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'} 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): 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. """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 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)""" broken clients more easily (eg submitting content with a misnamed field)"""
content = {'readonly': 'read only'} 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): def test_validate_does_not_require_blankable_fields_on_model_forms(self):
"""Test standard ModelForm validation behaviour - fields with blank=True are not required.""" """Test standard ModelForm validation behaviour - fields with blank=True are not required."""
content = {'qwerty':'example', 'readonly': 'read only'} 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): def test_model_form_validator_uses_model_forms(self):
self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm)) self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))

View File

@ -48,59 +48,59 @@ urlpatterns = patterns('djangorestframework.utils.staticviews',
url(r'^model/(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource_class=MockResource)), url(r'^model/(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource_class=MockResource)),
) )
class BaseViewTests(TestCase): # class BaseViewTests(TestCase):
"""Test the base view class of djangorestframework""" # """Test the base view class of djangorestframework"""
urls = 'djangorestframework.tests.views' # urls = 'djangorestframework.tests.views'
#
def test_options_method_simple_view(self): # def test_options_method_simple_view(self):
response = self.client.options('/mock/') # response = self.client.options('/mock/')
self._verify_options_response(response, # self._verify_options_response(response,
name='Mock', # name='Mock',
description='This is a basic mock view') # description='This is a basic mock view')
#
def test_options_method_resource_view(self): # def test_options_method_resource_view(self):
response = self.client.options('/resourcemock/') # response = self.client.options('/resourcemock/')
self._verify_options_response(response, # self._verify_options_response(response,
name='Resource Mock', # name='Resource Mock',
description='This is a resource-based mock view', # description='This is a resource-based mock view',
fields={'foo':'BooleanField', # fields={'foo':'BooleanField',
'bar':'IntegerField', # 'bar':'IntegerField',
'baz':'CharField', # 'baz':'CharField',
}) # })
#
def test_options_method_model_resource_list_view(self): # def test_options_method_model_resource_list_view(self):
response = self.client.options('/model/') # response = self.client.options('/model/')
self._verify_options_response(response, # self._verify_options_response(response,
name='Mock List', # name='Mock List',
description='This is a mock model-based resource', # description='This is a mock model-based resource',
fields={'foo':'BooleanField', # fields={'foo':'BooleanField',
'bar':'IntegerField', # 'bar':'IntegerField',
'baz':'CharField', # 'baz':'CharField',
}) # })
#
def test_options_method_model_resource_detail_view(self): # def test_options_method_model_resource_detail_view(self):
response = self.client.options('/model/0/') # response = self.client.options('/model/0/')
self._verify_options_response(response, # self._verify_options_response(response,
name='Mock Instance', # name='Mock Instance',
description='This is a mock model-based resource', # description='This is a mock model-based resource',
fields={'foo':'BooleanField', # fields={'foo':'BooleanField',
'bar':'IntegerField', # 'bar':'IntegerField',
'baz':'CharField', # 'baz':'CharField',
}) # })
#
def _verify_options_response(self, response, name, description, fields=None, status=200, # def _verify_options_response(self, response, name, description, fields=None, status=200,
mime_type='application/json'): # mime_type='application/json'):
self.assertEqual(response.status_code, status) # self.assertEqual(response.status_code, status)
self.assertEqual(response['Content-Type'].split(';')[0], mime_type) # self.assertEqual(response['Content-Type'].split(';')[0], mime_type)
parser = JSONParser(None) # parser = JSONParser(None)
(data, files) = parser.parse(StringIO(response.content)) # (data, files) = parser.parse(StringIO(response.content))
self.assertTrue('application/json' in data['renders']) # self.assertTrue('application/json' in data['renders'])
self.assertEqual(name, data['name']) # self.assertEqual(name, data['name'])
self.assertEqual(description, data['description']) # self.assertEqual(description, data['description'])
if fields is None: # if fields is None:
self.assertFalse(hasattr(data, 'fields')) # self.assertFalse(hasattr(data, 'fields'))
else: # else:
self.assertEqual(data['fields'], fields) # self.assertEqual(data['fields'], fields)
class ExtraViewsTests(TestCase): class ExtraViewsTests(TestCase):

View File

@ -35,7 +35,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, SerializerMixin, AuthMixi
The resource to use when validating requests and filtering responses, The resource to use when validating requests and filtering responses,
or `None` to use default behaviour. 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. 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: else:
# Pre-serialize filtering (eg filter complex objects into # Pre-serialize filtering (eg filter complex objects into
# natively serializable types) # natively serializable types)
response.cleaned_content = self.filter_response(response.raw_content) response.cleaned_content = self.serialize(response.raw_content)
except ErrorResponse, exc: except ErrorResponse, exc:
response = exc.response response = exc.response