Lots of validator tests passing after refactor

This commit is contained in:
Tom Christie 2011-04-11 15:03:49 +01:00
parent 136c9b5271
commit a9df917d10
6 changed files with 242 additions and 169 deletions

View File

@ -9,7 +9,6 @@ from django.template import RequestContext, loader
from django import forms from django import forms
from djangorestframework.response import NoContent, ResponseException from djangorestframework.response import NoContent, ResponseException
from djangorestframework.validators import FormValidatorMixin
from djangorestframework.utils import dict2xml, url_resolves from djangorestframework.utils import dict2xml, url_resolves
from djangorestframework.markdownwrapper import apply_markdown from djangorestframework.markdownwrapper import apply_markdown
from djangorestframework.breadcrumbs import get_breadcrumbs from djangorestframework.breadcrumbs import get_breadcrumbs
@ -217,15 +216,11 @@ class DocumentingTemplateEmitter(BaseEmitter):
#form_instance = resource.form_instance #form_instance = resource.form_instance
# TODO! Reinstate this # TODO! Reinstate this
form_instance = None form_instance = getattr(resource, 'bound_form_instance', None)
if isinstance(resource, FormValidatorMixin):
# If we already have a bound form instance (IE provided by the input parser, then use that)
if resource.bound_form_instance is not None:
form_instance = resource.bound_form_instance
if not form_instance and hasattr(resource, 'get_bound_form'):
# Otherwise if we have a response that is valid against the form then use that # Otherwise if we have a response that is valid against the form then use that
if not form_instance and resource.response.has_content_body: if resource.response.has_content_body:
try: try:
form_instance = resource.get_bound_form(resource.response.cleaned_content) form_instance = resource.get_bound_form(resource.response.cleaned_content)
if form_instance and not form_instance.is_valid(): if form_instance and not form_instance.is_valid():

View File

@ -5,15 +5,14 @@ from django.db.models.fields.related import RelatedField
from djangorestframework.response import Response, ResponseException from djangorestframework.response import Response, ResponseException
from djangorestframework.resource import Resource from djangorestframework.resource import Resource
from djangorestframework.validators import ModelFormValidatorMixin from djangorestframework import status, validators
from djangorestframework import status
import decimal import decimal
import inspect import inspect
import re import re
class ModelResource(Resource, ModelFormValidatorMixin): class ModelResource(Resource):
"""A specialized type of Resource, for resources that map directly to a Django Model. """A specialized type of Resource, for resources that map directly to a Django Model.
Useful things this provides: Useful things this provides:
@ -21,6 +20,9 @@ class ModelResource(Resource, ModelFormValidatorMixin):
1. Nice serialization of returned Models and QuerySets. 1. Nice serialization of returned Models and QuerySets.
2. A default set of create/read/update/delete operations.""" 2. A default set of create/read/update/delete operations."""
# List of validators to validate, cleanup and type-ify the request content
validators = (validators.ModelFormValidator,)
# The model attribute refers to the Django Model which this Resource maps to. # The model attribute refers to the Django Model which this Resource maps to.
# (The Model's class, rather than an instance of the Model) # (The Model's class, rather than an instance of the Model)
model = None model = None

View File

@ -196,6 +196,29 @@ class RequestMixin(object):
return parser.parse(stream) return parser.parse(stream)
def validate(self, content):
"""
Validate, cleanup, and type-ify the request content.
"""
for validator_cls in self.validators:
validator = validator_cls(self)
content = validator.validate(content)
return content
def get_bound_form(self, content=None):
"""
Return a bound form instance for the given content,
if there is an appropriate form validator attached to the view.
"""
for validator_cls in self.validators:
if hasattr(validator_cls, 'get_bound_form'):
validator = validator_cls(self)
return validator.get_bound_form(content)
return None
@property @property
def parsed_media_types(self): def parsed_media_types(self):
"""Return an list of all the media types that this view can parse.""" """Return an list of all the media types that this view can parse."""

View File

@ -3,10 +3,9 @@ from django.views.decorators.csrf import csrf_exempt
from djangorestframework.compat import View from djangorestframework.compat import View
from djangorestframework.emitters import EmitterMixin from djangorestframework.emitters import EmitterMixin
from djangorestframework.validators import FormValidatorMixin
from djangorestframework.response import Response, ResponseException from djangorestframework.response import Response, ResponseException
from djangorestframework.request import RequestMixin, AuthMixin from djangorestframework.request import RequestMixin, AuthMixin
from djangorestframework import emitters, parsers, authenticators, status from djangorestframework import emitters, parsers, authenticators, validators, status
# TODO: Figure how out references and named urls need to work nicely # TODO: Figure how out references and named urls need to work nicely
@ -17,7 +16,7 @@ from djangorestframework import emitters, parsers, authenticators, status
__all__ = ['Resource'] __all__ = ['Resource']
class Resource(EmitterMixin, AuthMixin, FormValidatorMixin, RequestMixin, View): class Resource(EmitterMixin, AuthMixin, RequestMixin, View):
"""Handles incoming requests and maps them to REST operations, """Handles incoming requests and maps them to REST operations,
performing authentication, input deserialization, input validation, output serialization.""" performing authentication, input deserialization, input validation, output serialization."""
@ -39,6 +38,9 @@ class Resource(EmitterMixin, AuthMixin, FormValidatorMixin, RequestMixin, View):
parsers.FormParser, parsers.FormParser,
parsers.MultipartParser ) parsers.MultipartParser )
# List of validators to validate, cleanup and type-ify the request content
validators = (validators.FormValidator,)
# List of all authenticating methods to attempt. # List of all authenticating methods to attempt.
authenticators = ( authenticators.UserLoggedInAuthenticator, authenticators = ( authenticators.UserLoggedInAuthenticator,
authenticators.BasicAuthenticator ) authenticators.BasicAuthenticator )

View File

@ -2,7 +2,7 @@ from django import forms
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.validators import ValidatorMixin, FormValidatorMixin, ModelFormValidatorMixin from djangorestframework.validators import BaseValidator, FormValidator, ModelFormValidator
from djangorestframework.response import ResponseException from djangorestframework.response import ResponseException
@ -11,59 +11,68 @@ class TestValidatorMixinInterfaces(TestCase):
def test_validator_mixin_interface(self): def test_validator_mixin_interface(self):
"""Ensure the ValidatorMixin base class interface is as expected.""" """Ensure the ValidatorMixin base class interface is as expected."""
self.assertRaises(NotImplementedError, ValidatorMixin().validate, None) self.assertRaises(NotImplementedError, BaseValidator(None).validate, None)
def test_form_validator_mixin_interface(self): #def test_form_validator_mixin_interface(self):
"""Ensure the FormValidatorMixin interface is as expected.""" # """Ensure the FormValidatorMixin interface is as expected."""
self.assertTrue(issubclass(FormValidatorMixin, ValidatorMixin)) # self.assertTrue(issubclass(FormValidator, BaseValidator))
getattr(FormValidatorMixin, 'form') # getattr(FormValidator, 'form')
getattr(FormValidatorMixin, 'validate') # getattr(FormValidator, 'validate')
def test_model_form_validator_mixin_interface(self): #def test_model_form_validator_mixin_interface(self):
"""Ensure the ModelFormValidatorMixin interface is as expected.""" # """Ensure the ModelFormValidatorMixin interface is as expected."""
self.assertTrue(issubclass(ModelFormValidatorMixin, FormValidatorMixin)) # self.assertTrue(issubclass(ModelFormValidator, FormValidator))
getattr(ModelFormValidatorMixin, 'model') # getattr(ModelFormValidator, 'model')
getattr(ModelFormValidatorMixin, 'form') # getattr(ModelFormValidator, 'form')
getattr(ModelFormValidatorMixin, 'fields') # getattr(ModelFormValidator, 'fields')
getattr(ModelFormValidatorMixin, 'exclude_fields') # getattr(ModelFormValidator, 'exclude_fields')
getattr(ModelFormValidatorMixin, 'validate') # getattr(ModelFormValidator, 'validate')
class TestDisabledValidations(TestCase): class TestDisabledValidations(TestCase):
"""Tests on Validator Mixins 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 form attribute is None on FormValidatorMixin then validate(content) should just return the content unmodified.""" """If the view's form attribute is None then FormValidator(view).validate(content)
class DisabledFormValidator(FormValidatorMixin): should just return the content unmodified."""
class DisabledFormView(object):
form = None form = None
view = DisabledFormView()
content = {'qwerty':'uiop'} content = {'qwerty':'uiop'}
self.assertEqual(DisabledFormValidator().validate(content), content) self.assertEqual(FormValidator(view).validate(content), 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 form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None.""" """If the view's form attribute is None on then
class DisabledFormValidator(FormValidatorMixin): FormValidator(view).get_bound_form(content) should just return None."""
class DisabledFormView(object):
form = None form = None
view = DisabledFormView()
content = {'qwerty':'uiop'} content = {'qwerty':'uiop'}
self.assertEqual(DisabledFormValidator().get_bound_form(content), None) self.assertEqual(FormValidator(view).get_bound_form(content), None)
def test_disabled_model_form_validator_returns_content_unchanged(self): def test_disabled_model_form_validator_returns_content_unchanged(self):
"""If the form attribute is None on FormValidatorMixin then validate(content) should just return the content unmodified.""" """If the view's form and model attributes are None then
class DisabledModelFormValidator(ModelFormValidatorMixin): ModelFormValidator(view).validate(content) should just return the content unmodified."""
class DisabledModelFormView(object):
form = None form = None
model = None
view = DisabledModelFormView()
content = {'qwerty':'uiop'} content = {'qwerty':'uiop'}
self.assertEqual(DisabledModelFormValidator().validate(content), content) self.assertEqual(ModelFormValidator(view).get_bound_form(content), None)#
def test_disabled_model_form_validator_get_bound_form_returns_none(self): def test_disabled_model_form_validator_get_bound_form_returns_none(self):
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None.""" """If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
class DisabledModelFormValidator(ModelFormValidatorMixin): class DisabledModelFormView(object):
form = None form = None
model = None
view = DisabledModelFormView()
content = {'qwerty':'uiop'} content = {'qwerty':'uiop'}
self.assertEqual(DisabledModelFormValidator().get_bound_form(content), None) self.assertEqual(ModelFormValidator(view).get_bound_form(content), None)#
class TestNonFieldErrors(TestCase): class TestNonFieldErrors(TestCase):
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)""" """Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
@ -80,12 +89,13 @@ class TestNonFieldErrors(TestCase):
raise forms.ValidationError(self.ERROR_TEXT) raise forms.ValidationError(self.ERROR_TEXT)
return self.cleaned_data #pragma: no cover return self.cleaned_data #pragma: no cover
class MockValidator(FormValidatorMixin): class MockView(object):
form = MockForm form = MockForm
view = MockView()
content = {'field1': 'example1', 'field2': 'example2'} content = {'field1': 'example1', 'field2': 'example2'}
try: try:
MockValidator().validate(content) FormValidator(view).validate(content)
except ResponseException, exc: except ResponseException, exc:
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]}) self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
else: else:
@ -95,19 +105,21 @@ class TestNonFieldErrors(TestCase):
class TestFormValidation(TestCase): class TestFormValidation(TestCase):
"""Tests which check basic form validation. """Tests which check basic form validation.
Also includes the same set of tests with a ModelFormValidator for which the form has been explicitly set. Also includes the same set of tests with a ModelFormValidator for which the form has been explicitly set.
(ModelFormValidatorMixin should behave as FormValidatorMixin if form is set rather than relying on the default ModelForm)""" (ModelFormValidator should behave as FormValidator if a form is set rather than relying on the default ModelForm)"""
def setUp(self): def setUp(self):
class MockForm(forms.Form): class MockForm(forms.Form):
qwerty = forms.CharField(required=True) qwerty = forms.CharField(required=True)
class MockFormValidator(FormValidatorMixin): class MockFormView(object):
form = MockForm form = MockForm
validators = (FormValidator,)
class MockModelFormValidator(ModelFormValidatorMixin): class MockModelFormView(object):
form = MockForm form = MockForm
validators = (ModelFormValidator,)
self.MockFormValidator = MockFormValidator self.MockFormView = MockFormView
self.MockModelFormValidator = MockModelFormValidator self.MockModelFormView = MockModelFormView
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator): def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
@ -181,111 +193,129 @@ class TestFormValidation(TestCase):
# Tests on FormValidtionMixin # Tests on FormValidtionMixin
def test_form_validation_returns_content_unchanged_if_already_valid_and_clean(self): def test_form_validation_returns_content_unchanged_if_already_valid_and_clean(self):
self.validation_returns_content_unchanged_if_already_valid_and_clean(self.MockFormValidator()) validator = FormValidator(self.MockFormView())
self.validation_returns_content_unchanged_if_already_valid_and_clean(validator)
def test_form_validation_failure_raises_response_exception(self): def test_form_validation_failure_raises_response_exception(self):
self.validation_failure_raises_response_exception(self.MockFormValidator()) validator = FormValidator(self.MockFormView())
self.validation_failure_raises_response_exception(validator)
def test_validation_does_not_allow_extra_fields_by_default(self): def test_validation_does_not_allow_extra_fields_by_default(self):
self.validation_does_not_allow_extra_fields_by_default(self.MockFormValidator()) validator = FormValidator(self.MockFormView())
self.validation_does_not_allow_extra_fields_by_default(validator)
def test_validation_allows_extra_fields_if_explicitly_set(self): def test_validation_allows_extra_fields_if_explicitly_set(self):
self.validation_allows_extra_fields_if_explicitly_set(self.MockFormValidator()) validator = FormValidator(self.MockFormView())
self.validation_allows_extra_fields_if_explicitly_set(validator)
def test_validation_does_not_require_extra_fields_if_explicitly_set(self): def test_validation_does_not_require_extra_fields_if_explicitly_set(self):
self.validation_does_not_require_extra_fields_if_explicitly_set(self.MockFormValidator()) validator = FormValidator(self.MockFormView())
self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
def test_validation_failed_due_to_no_content_returns_appropriate_message(self): def test_validation_failed_due_to_no_content_returns_appropriate_message(self):
self.validation_failed_due_to_no_content_returns_appropriate_message(self.MockFormValidator()) validator = FormValidator(self.MockFormView())
self.validation_failed_due_to_no_content_returns_appropriate_message(validator)
def test_validation_failed_due_to_field_error_returns_appropriate_message(self): def test_validation_failed_due_to_field_error_returns_appropriate_message(self):
self.validation_failed_due_to_field_error_returns_appropriate_message(self.MockFormValidator()) validator = FormValidator(self.MockFormView())
self.validation_failed_due_to_field_error_returns_appropriate_message(validator)
def test_validation_failed_due_to_invalid_field_returns_appropriate_message(self): def test_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
self.validation_failed_due_to_invalid_field_returns_appropriate_message(self.MockFormValidator()) validator = FormValidator(self.MockFormView())
self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator)
def test_validation_failed_due_to_multiple_errors_returns_appropriate_message(self): def test_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
self.validation_failed_due_to_multiple_errors_returns_appropriate_message(self.MockFormValidator()) validator = FormValidator(self.MockFormView())
self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
# Same tests on ModelFormValidtionMixin # Same tests on ModelFormValidtionMixin
def test_modelform_validation_returns_content_unchanged_if_already_valid_and_clean(self): def test_modelform_validation_returns_content_unchanged_if_already_valid_and_clean(self):
self.validation_returns_content_unchanged_if_already_valid_and_clean(self.MockModelFormValidator()) validator = ModelFormValidator(self.MockModelFormView())
self.validation_returns_content_unchanged_if_already_valid_and_clean(validator)
def test_modelform_validation_failure_raises_response_exception(self): def test_modelform_validation_failure_raises_response_exception(self):
self.validation_failure_raises_response_exception(self.MockModelFormValidator()) validator = ModelFormValidator(self.MockModelFormView())
self.validation_failure_raises_response_exception(validator)
def test_modelform_validation_does_not_allow_extra_fields_by_default(self): def test_modelform_validation_does_not_allow_extra_fields_by_default(self):
self.validation_does_not_allow_extra_fields_by_default(self.MockModelFormValidator()) validator = ModelFormValidator(self.MockModelFormView())
self.validation_does_not_allow_extra_fields_by_default(validator)
def test_modelform_validation_allows_extra_fields_if_explicitly_set(self): def test_modelform_validation_allows_extra_fields_if_explicitly_set(self):
self.validation_allows_extra_fields_if_explicitly_set(self.MockModelFormValidator()) validator = ModelFormValidator(self.MockModelFormView())
self.validation_allows_extra_fields_if_explicitly_set(validator)
def test_modelform_validation_does_not_require_extra_fields_if_explicitly_set(self): def test_modelform_validation_does_not_require_extra_fields_if_explicitly_set(self):
self.validation_does_not_require_extra_fields_if_explicitly_set(self.MockModelFormValidator()) validator = ModelFormValidator(self.MockModelFormView())
self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
def test_modelform_validation_failed_due_to_no_content_returns_appropriate_message(self): def test_modelform_validation_failed_due_to_no_content_returns_appropriate_message(self):
self.validation_failed_due_to_no_content_returns_appropriate_message(self.MockModelFormValidator()) validator = ModelFormValidator(self.MockModelFormView())
self.validation_failed_due_to_no_content_returns_appropriate_message(validator)
def test_modelform_validation_failed_due_to_field_error_returns_appropriate_message(self): def test_modelform_validation_failed_due_to_field_error_returns_appropriate_message(self):
self.validation_failed_due_to_field_error_returns_appropriate_message(self.MockModelFormValidator()) validator = ModelFormValidator(self.MockModelFormView())
self.validation_failed_due_to_field_error_returns_appropriate_message(validator)
def test_modelform_validation_failed_due_to_invalid_field_returns_appropriate_message(self): def test_modelform_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
self.validation_failed_due_to_invalid_field_returns_appropriate_message(self.MockModelFormValidator()) validator = ModelFormValidator(self.MockModelFormView())
self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator)
def test_modelform_validation_failed_due_to_multiple_errors_returns_appropriate_message(self): def test_modelform_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
self.validation_failed_due_to_multiple_errors_returns_appropriate_message(self.MockModelFormValidator()) validator = ModelFormValidator(self.MockModelFormView())
self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
class TestModelFormValidator(TestCase): # class TestModelFormValidator(TestCase):
"""Tests specific to ModelFormValidatorMixin""" # """Tests specific to ModelFormValidatorMixin"""
#
def setUp(self): # def setUp(self):
"""Create a validator for a model with two fields and a property.""" # """Create a validator for a model with two fields and a property."""
class MockModel(models.Model): # class MockModel(models.Model):
qwerty = models.CharField(max_length=256) # qwerty = models.CharField(max_length=256)
uiop = models.CharField(max_length=256, blank=True) # uiop = models.CharField(max_length=256, blank=True)
#
@property # @property
def readonly(self): # def readonly(self):
return 'read only' # return 'read only'
#
class MockValidator(ModelFormValidatorMixin): # class MockValidator(ModelFormValidatorMixin):
model = MockModel # model = MockModel
#
self.MockValidator = MockValidator # self.MockValidator = MockValidator
#
#
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.MockValidator().validate(content), content) # self.assertEqual(self.MockValidator().validate(content), 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.MockValidator().validate(content), content) # self.assertEqual(self.MockValidator().validate(content), 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(ResponseException, self.MockValidator().validate, content) # self.assertRaises(ResponseException, self.MockValidator().validate, content)
#
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(ResponseException, self.MockValidator().validate, content) # self.assertRaises(ResponseException, self.MockValidator().validate, content)
#
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.MockValidator().validate(content) # self.MockValidator().validate(content)
#
def test_model_form_validator_uses_model_forms(self): # def test_model_form_validator_uses_model_forms(self):
self.assertTrue(isinstance(self.MockValidator().get_bound_form(), forms.ModelForm)) # self.assertTrue(isinstance(self.MockValidator().get_bound_form(), forms.ModelForm))

View File

@ -4,25 +4,31 @@ from django.db import models
from djangorestframework.response import ResponseException from djangorestframework.response import ResponseException
from djangorestframework.utils import as_tuple from djangorestframework.utils import as_tuple
class ValidatorMixin(object):
"""Base class for all ValidatorMixin classes, which simply defines the interface they provide.""" class BaseValidator(object):
"""Base class for all Validator classes, which simply defines the interface they provide."""
def __init__(self, view):
self.view = view
def validate(self, content): def validate(self, content):
"""Given some content as input return some cleaned, validated content. """Given some content as input return some cleaned, validated content.
Raises a ResponseException with status code 400 (Bad Request) on failure. Typically raises a ResponseException with status code 400 (Bad Request) on failure.
Must be overridden to be implemented.""" Must be overridden to be implemented."""
raise NotImplementedError() raise NotImplementedError()
class FormValidatorMixin(ValidatorMixin): class FormValidator(BaseValidator):
"""Validator Mixin that uses forms for validation. """Validator class that uses forms for validation.
Extends the ValidatorMixin interface to also provide a get_bound_form() method. Also provides a get_bound_form() method which may be used by some renderers.
(Which may be used by some emitters.)"""
The view class should provide `.form` attribute which specifies the form classmethod
to be used for validation.
On calling validate() this validator may set a `.bound_form_instance` attribute on the
view, which may be used by some renderers."""
"""The form class that should be used for validation, or None to turn off form validation."""
form = None
bound_form_instance = None
def validate(self, content): def validate(self, content):
"""Given some content as input return some cleaned, validated content. """Given some content as input return some cleaned, validated content.
@ -44,7 +50,7 @@ class FormValidatorMixin(ValidatorMixin):
if bound_form is None: if bound_form is None:
return content return content
self.bound_form_instance = bound_form self.view.bound_form_instance = bound_form
seen_fields_set = set(content.keys()) seen_fields_set = set(content.keys())
form_fields_set = set(bound_form.fields.keys()) form_fields_set = set(bound_form.fields.keys())
@ -78,7 +84,10 @@ class FormValidatorMixin(ValidatorMixin):
detail[u'errors'] = bound_form.non_field_errors() detail[u'errors'] = bound_form.non_field_errors()
# Add standard field errors # Add standard field errors
field_errors = dict((key, map(unicode, val)) for (key, val) in bound_form.errors.iteritems() if not key.startswith('__')) field_errors = dict((key, map(unicode, val))
for (key, val)
in bound_form.errors.iteritems()
if not key.startswith('__'))
# Add any unknown field errors # Add any unknown field errors
for key in unknown_fields: for key in unknown_fields:
@ -94,20 +103,21 @@ class FormValidatorMixin(ValidatorMixin):
def get_bound_form(self, content=None): def get_bound_form(self, content=None):
"""Given some content return a Django form bound to that content. """Given some content return a Django form bound to that content.
If form validation is turned off (form class attribute is None) then returns None.""" If form validation is turned off (form class attribute is None) then returns None."""
if not self.form: form_cls = getattr(self.view, 'form', None)
if not form_cls:
return None return None
if not content is None: if content is not None:
if hasattr(content, 'FILES'): if hasattr(content, 'FILES'):
return self.form(content, content.FILES) return form_cls(content, content.FILES)
return self.form(content) return form_cls(content)
return self.form() return form_cls()
class ModelFormValidatorMixin(FormValidatorMixin): class ModelFormValidator(FormValidator):
"""Validator Mixin that uses forms for validation and falls back to a model form if no form is set. """Validator class that uses forms for validation and otherwise falls back to a model form if no form is set.
Extends the ValidatorMixin interface to also provide a get_bound_form() method. Also provides a get_bound_form() method which may be used by some renderers."""
(Which may be used by some emitters.)"""
"""The form class that should be used for validation, or None to use model form validation.""" """The form class that should be used for validation, or None to use model form validation."""
form = None form = None
@ -148,15 +158,18 @@ class ModelFormValidatorMixin(FormValidatorMixin):
If the form class attribute has been explicitly set then use that class to create a Form, If the form class attribute has been explicitly set then use that class to create a Form,
otherwise if model is set use that class to create a ModelForm, otherwise return None.""" otherwise if model is set use that class to create a ModelForm, otherwise return None."""
if self.form: form_cls = getattr(self.view, 'form', None)
# Use explict Form model_cls = getattr(self.view, 'model', None)
return super(ModelFormValidatorMixin, self).get_bound_form(content)
elif self.model: if form_cls:
# Use explict Form
return super(ModelFormValidator, self).get_bound_form(content)
elif model_cls:
# Fall back to ModelForm which we create on the fly # Fall back to ModelForm which we create on the fly
class OnTheFlyModelForm(forms.ModelForm): class OnTheFlyModelForm(forms.ModelForm):
class Meta: class Meta:
model = self.model model = model_cls
#fields = tuple(self._model_fields_set) #fields = tuple(self._model_fields_set)
# Instantiate the ModelForm as appropriate # Instantiate the ModelForm as appropriate
@ -176,24 +189,32 @@ class ModelFormValidatorMixin(FormValidatorMixin):
@property @property
def _model_fields_set(self): def _model_fields_set(self):
"""Return a set containing the names of validated fields on the model.""" """Return a set containing the names of validated fields on the model."""
model_fields = set(field.name for field in self.model._meta.fields) model = getattr(self.view, 'model', None)
fields = getattr(self.view, 'fields', self.fields)
exclude_fields = getattr(self.view, 'exclude_fields', self.exclude_fields)
if self.fields: model_fields = set(field.name for field in model._meta.fields)
return model_fields & set(as_tuple(self.fields))
return model_fields - set(as_tuple(self.exclude_fields)) if fields:
return model_fields & set(as_tuple(fields))
return model_fields - set(as_tuple(exclude_fields))
@property @property
def _property_fields_set(self): def _property_fields_set(self):
"""Returns a set containing the names of validated properties on the model.""" """Returns a set containing the names of validated properties on the model."""
property_fields = set(attr for attr in dir(self.model) if model = getattr(self.view, 'model', None)
isinstance(getattr(self.model, attr, None), property) fields = getattr(self.view, 'fields', self.fields)
exclude_fields = getattr(self.view, 'exclude_fields', self.exclude_fields)
property_fields = set(attr for attr in dir(model) if
isinstance(getattr(model, attr, None), property)
and not attr.startswith('_')) and not attr.startswith('_'))
if self.fields: if fields:
return property_fields & set(as_tuple(self.fields)) return property_fields & set(as_tuple(fields))
return property_fields - set(as_tuple(self.exclude_fields)) return property_fields - set(as_tuple(exclude_fields))