mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-03 13:14:30 +03:00
153 lines
7.0 KiB
Python
153 lines
7.0 KiB
Python
"""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 |