mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-12-01 22:14:06 +03:00
Lots of validator tests passing after refactor
This commit is contained in:
parent
136c9b5271
commit
a9df917d10
|
@ -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():
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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 )
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user