aboutsummaryrefslogtreecommitdiffstats
path: root/djangorestframework/validators.py
diff options
context:
space:
mode:
authortom christie tom@tomchristie.com2011-02-07 08:23:54 +0000
committertom christie tom@tomchristie.com2011-02-07 08:23:54 +0000
commit027ffed21064b1ec304a1ea559104382313d76f4 (patch)
tree8621c8741d76cc672da207cf314574a4fbd828d1 /djangorestframework/validators.py
parenta8bcb2edc63564522b89b9950ea0882d6b25f24a (diff)
downloaddjango-rest-framework-027ffed21064b1ec304a1ea559104382313d76f4.tar.bz2
Refactor a bunch of stuff into mixins, more tests
Diffstat (limited to 'djangorestframework/validators.py')
-rw-r--r--djangorestframework/validators.py153
1 files changed, 153 insertions, 0 deletions
diff --git a/djangorestframework/validators.py b/djangorestframework/validators.py
new file mode 100644
index 00000000..aefad4b3
--- /dev/null
+++ b/djangorestframework/validators.py
@@ -0,0 +1,153 @@
+"""Mixin classes that provide a validate(content) function to validate and cleanup request content"""
+from django import forms
+from django.db import models
+from djangorestframework.response import ResponseException
+from djangorestframework.utils import as_tuple
+
+class ValidatorMixin(object):
+ """Base class for all ValidatorMixin classes, which simply defines the interface they provide."""
+
+ def validate(self, content):
+ """Given some content as input return some cleaned, validated content.
+ Raises a ResponseException with status code 400 (Bad Request) on failure.
+
+ Must be overridden to be implemented."""
+ raise NotImplementedError()
+
+
+class FormValidatorMixin(ValidatorMixin):
+ """Validator Mixin that uses forms for validation.
+ Extends the ValidatorMixin interface to also provide a get_bound_form() method.
+ (Which may be used by some emitters.)"""
+
+ """The form class that should be used for validation, or None to turn off form validation."""
+ form = None
+
+ def validate(self, content):
+ """Given some content as input return some cleaned, validated content.
+ Raises a ResponseException with status code 400 (Bad Request) on failure.
+
+ Validation is standard form validation, with an additional constraint that no extra unknown fields may be supplied.
+
+ On failure the ResponseException content is a dict which may contain 'errors' and 'field-errors' keys.
+ If the 'errors' key exists it is a list of strings of non-field errors.
+ If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}."""
+ return self._validate(content)
+
+ def _validate(self, content, extra_fields=()):
+ """Wrapped by validate to hide the extra_fields option that the ModelValidatorMixin uses.
+ extra_fields is a list of fields which are not defined by the form, but which we still
+ expect to see on the input."""
+ if self.form is None:
+ return content
+
+ bound_form = self.get_bound_form(content)
+
+ # In addition to regular validation we also ensure no additional fields are being passed in...
+ unknown_fields = set(content.keys()) - set(self.form().fields.keys()) - set(extra_fields)
+
+ # And that any extra fields we have specified are all present.
+ missing_extra_fields = set(extra_fields) - set(content.keys())
+
+ # Check using both regular validation, and our stricter no additional fields rule
+ if bound_form.is_valid() and not unknown_fields and not missing_extra_fields:
+ return bound_form.cleaned_data
+
+ # Validation failed...
+ detail = {}
+
+ if not bound_form.errors and not unknown_fields and not missing_extra_fields:
+ detail = {u'errors': [u'No content was supplied.']}
+
+ else:
+ # Add any non-field errors
+ if bound_form.non_field_errors():
+ detail[u'errors'] = bound_form.non_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('__'))
+
+ # Add any unknown field errors
+ for key in unknown_fields:
+ field_errors[key] = [u'This field does not exist.']
+
+ # Add any missing fields that we required by the extra fields argument
+ for key in missing_extra_fields:
+ field_errors[key] = [u'This field is required.']
+
+ if field_errors:
+ detail[u'field-errors'] = field_errors
+
+ # Return HTTP 400 response (BAD REQUEST)
+ raise ResponseException(400, detail)
+
+
+ def get_bound_form(self, content=None):
+ """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 not self.form:
+ return None
+
+ if content:
+ return self.form(content)
+ return self.form()
+
+
+class ModelFormValidatorMixin(FormValidatorMixin):
+ """Validator Mixin that uses forms for validation and falls back to a model form if no form is set.
+ Extends the ValidatorMixin interface to also provide a get_bound_form() method.
+ (Which may be used by some emitters.)"""
+
+ """The form class that should be used for validation, or None to use model form validation."""
+ form = None
+
+ """The model class from which the model form should be constructed if no form is set."""
+ model = None
+
+ """The list of fields we expect to receive as input. Fields in this list will may be received with
+ raising non-existent field errors, even if they do not exist as fields on the ModelForm."""
+ fields = None
+
+ # TODO: test the different validation here to allow for get get_absolute_url to be supplied on input and not bork out
+ # TODO: be really strict on fields - check they match in the handler methods. (this isn't a validator thing tho.)
+ def validate(self, content):
+ """Given some content as input return some cleaned, validated content.
+ Raises a ResponseException with status code 400 (Bad Request) on failure.
+
+ Validation is standard form or model form validation,
+ with an additional constraint that no extra unknown fields may be supplied,
+ and that all fields specified by the fields class attribute must be supplied,
+ even if they are not validated by the form/model form.
+
+ On failure the ResponseException content is a dict which may contain 'errors' and 'field-errors' keys.
+ If the 'errors' key exists it is a list of strings of non-field errors.
+ If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}."""
+ extra_fields = set(as_tuple(self.fields)) - set(self.get_bound_form().fields)
+ return self._validate(content, extra_fields)
+
+
+ def get_bound_form(self, content=None):
+ """Given some content return a Django form bound to that content.
+
+ 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."""
+ if self.form:
+ # Use explict Form
+ return super(ModelFormValidatorMixin, self).get_bound_form(content)
+
+ elif self.model:
+ # Fall back to ModelForm which we create on the fly
+ class ModelForm(forms.ModelForm):
+ class Meta:
+ model = self.model
+ fields = tuple(set.intersection(self.model._meta.fields, self.fields))
+
+ # Instantiate the ModelForm as appropriate
+ if content and isinstance(content, models.Model):
+ return ModelForm(instance=content)
+ elif content:
+ return ModelForm(content)
+ return ModelForm()
+
+ # Both form and model not set? Okay bruv, whatevs...
+ return None \ No newline at end of file