mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-25 11:04:02 +03:00
Refactor a bunch of stuff into mixins, more tests
This commit is contained in:
parent
a8bcb2edc6
commit
027ffed210
|
@ -29,10 +29,10 @@ class OverloadedContentMixin(ContentMixin):
|
|||
"""HTTP request content behaviour that also allows arbitrary content to be tunneled in form data."""
|
||||
|
||||
"""The name to use for the content override field in the POST form."""
|
||||
FORM_PARAM_CONTENT = '_content'
|
||||
CONTENT_PARAM = '_content'
|
||||
|
||||
"""The name to use for the content-type override field in the POST form."""
|
||||
FORM_PARAM_CONTENTTYPE = '_contenttype'
|
||||
CONTENTTYPE_PARAM = '_contenttype'
|
||||
|
||||
def determine_content(self, request):
|
||||
"""If the request contains content return a tuple of (content_type, content) otherwise return None.
|
||||
|
@ -42,14 +42,14 @@ class OverloadedContentMixin(ContentMixin):
|
|||
|
||||
content_type = request.META.get('CONTENT_TYPE', None)
|
||||
|
||||
if (request.method == 'POST' and self.FORM_PARAM_CONTENT and
|
||||
request.POST.get(self.FORM_PARAM_CONTENT, None) is not None):
|
||||
if (request.method == 'POST' and self.CONTENT_PARAM and
|
||||
request.POST.get(self.CONTENT_PARAM, None) is not None):
|
||||
|
||||
# Set content type if form contains a none empty FORM_PARAM_CONTENTTYPE field
|
||||
content_type = None
|
||||
if self.FORM_PARAM_CONTENTTYPE and request.POST.get(self.FORM_PARAM_CONTENTTYPE, None):
|
||||
content_type = request.POST.get(self.FORM_PARAM_CONTENTTYPE, None)
|
||||
if self.CONTENTTYPE_PARAM and request.POST.get(self.CONTENTTYPE_PARAM, None):
|
||||
content_type = request.POST.get(self.CONTENTTYPE_PARAM, None)
|
||||
|
||||
return (content_type, request.POST[self.FORM_PARAM_CONTENT])
|
||||
return (content_type, request.POST[self.CONTENT_PARAM])
|
||||
|
||||
return (content_type, request.raw_post_data)
|
|
@ -8,6 +8,7 @@ from django.template import RequestContext, loader
|
|||
from django import forms
|
||||
|
||||
from djangorestframework.response import NoContent
|
||||
from djangorestframework.validators import FormValidatorMixin
|
||||
from djangorestframework.utils import dict2xml, url_resolves
|
||||
|
||||
from urllib import quote_plus
|
||||
|
@ -82,24 +83,28 @@ class DocumentingTemplateEmitter(BaseEmitter):
|
|||
In the absence on of the Resource having an associated form then
|
||||
provide a form that can be used to submit arbitrary content."""
|
||||
# Get the form instance if we have one bound to the input
|
||||
form_instance = resource.form_instance
|
||||
#form_instance = resource.form_instance
|
||||
# TODO! Reinstate this
|
||||
|
||||
# Otherwise if this isn't an error response
|
||||
# then attempt to get a form bound to the response object
|
||||
if not form_instance and resource.response.has_content_body:
|
||||
try:
|
||||
form_instance = resource.get_form(resource.response.raw_content)
|
||||
if form_instance:
|
||||
form_instance.is_valid()
|
||||
except:
|
||||
form_instance = None
|
||||
|
||||
# If we still don't have a form instance then try to get an unbound form
|
||||
if not form_instance:
|
||||
try:
|
||||
form_instance = self.resource.get_form()
|
||||
except:
|
||||
pass
|
||||
form_instance = None
|
||||
|
||||
if isinstance(self, FormValidatorMixin):
|
||||
# Otherwise if this isn't an error response
|
||||
# then attempt to get a form bound to the response object
|
||||
if not form_instance and resource.response.has_content_body:
|
||||
try:
|
||||
form_instance = resource.get_bound_form(resource.response.raw_content)
|
||||
if form_instance:
|
||||
form_instance.is_valid()
|
||||
except:
|
||||
form_instance = None
|
||||
|
||||
# If we still don't have a form instance then try to get an unbound form
|
||||
if not form_instance:
|
||||
try:
|
||||
form_instance = self.resource.get_bound_form()
|
||||
except:
|
||||
pass
|
||||
|
||||
# If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
|
||||
if not form_instance:
|
||||
|
|
|
@ -24,12 +24,12 @@ class OverloadedPOSTMethodMixin(MethodMixin):
|
|||
"""Provide for overloaded POST behaviour."""
|
||||
|
||||
"""The name to use for the method override field in the POST form."""
|
||||
FORM_PARAM_METHOD = '_method'
|
||||
METHOD_PARAM = '_method'
|
||||
|
||||
def determine_method(self, request):
|
||||
"""Simply return GET, POST etc... as appropriate, allowing for POST overloading
|
||||
by setting a form field with the requested method name."""
|
||||
method = request.method.upper()
|
||||
if method == 'POST' and self.FORM_PARAM_METHOD and request.POST.has_key(self.FORM_PARAM_METHOD):
|
||||
method = request.POST[self.FORM_PARAM_METHOD].upper()
|
||||
if method == 'POST' and self.METHOD_PARAM and request.POST.has_key(self.METHOD_PARAM):
|
||||
method = request.POST[self.METHOD_PARAM].upper()
|
||||
return method
|
|
@ -5,7 +5,38 @@ try:
|
|||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
# TODO: Make all parsers only list a single media_type, rather than a list
|
||||
|
||||
class ParserMixin(object):
|
||||
parsers = ()
|
||||
|
||||
def parse(self, content_type, content):
|
||||
# See RFC 2616 sec 3 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
|
||||
split = content_type.split(';', 1)
|
||||
if len(split) > 1:
|
||||
content_type = split[0]
|
||||
content_type = content_type.strip()
|
||||
|
||||
media_type_to_parser = dict([(parser.media_type, parser) for parser in self.parsers])
|
||||
|
||||
try:
|
||||
parser = media_type_to_parser[content_type]
|
||||
except KeyError:
|
||||
raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
||||
{'error': 'Unsupported media type in request \'%s\'.' % content_type})
|
||||
|
||||
return parser(self).parse(content)
|
||||
|
||||
@property
|
||||
def parsed_media_types(self):
|
||||
"""Return an list of all the media types that this ParserMixin can parse."""
|
||||
return [parser.media_type for parser in self.parsers]
|
||||
|
||||
@property
|
||||
def default_parser(self):
|
||||
"""Return the ParerMixin's most prefered emitter.
|
||||
(This has no behavioural effect, but is may be used by documenting emitters)"""
|
||||
return self.parsers[0]
|
||||
|
||||
|
||||
class BaseParser(object):
|
||||
"""All parsers should extend BaseParser, specifing a media_type attribute,
|
||||
|
|
|
@ -2,6 +2,10 @@ from django.contrib.sites.models import Site
|
|||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse
|
||||
|
||||
from djangorestframework.parsers import ParserMixin
|
||||
from djangorestframework.validators import FormValidatorMixin
|
||||
from djangorestframework.content import OverloadedContentMixin
|
||||
from djangorestframework.methods import OverloadedPOSTMethodMixin
|
||||
from djangorestframework import emitters, parsers, authenticators
|
||||
from djangorestframework.response import status, Response, ResponseException
|
||||
|
||||
|
@ -20,7 +24,7 @@ __all__ = ['Resource']
|
|||
_MSIE_USER_AGENT = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
|
||||
|
||||
|
||||
class Resource(object):
|
||||
class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, OverloadedPOSTMethodMixin):
|
||||
"""Handles incoming requests and maps them to REST operations,
|
||||
performing authentication, input deserialization, input validation, output serialization."""
|
||||
|
||||
|
@ -53,10 +57,7 @@ class Resource(object):
|
|||
|
||||
# Some reserved parameters to allow us to use standard HTML forms with our resource
|
||||
# Override any/all of these with None to disable them, or override them with another value to rename them.
|
||||
ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
|
||||
METHOD_PARAM = '_method' # Allow POST overloading in form params
|
||||
CONTENTTYPE_PARAM = '_contenttype' # Allow override of Content-Type header in form params (allows sending arbitrary content with standard forms)
|
||||
CONTENT_PARAM = '_content' # Allow override of body content in form params (allows sending arbitrary content with standard forms)
|
||||
ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params CONTENTTYPE_PARAM = '_contenttype' # Allow override of Content-Type header in form params (allows sending arbitrary content with standard forms)
|
||||
CSRF_PARAM = 'csrfmiddlewaretoken' # Django's CSRF token used in form params
|
||||
|
||||
_MUNGE_IE_ACCEPT_HEADER = True
|
||||
|
@ -112,18 +113,6 @@ class Resource(object):
|
|||
(This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
|
||||
return self.emitters[0]
|
||||
|
||||
@property
|
||||
def parsed_media_types(self):
|
||||
"""Return an list of all the media types that this resource can emit."""
|
||||
return [parser.media_type for parser in self.parsers]
|
||||
|
||||
@property
|
||||
def default_parser(self):
|
||||
"""Return the resource's most prefered emitter.
|
||||
(This has no behavioural effect, but is may be used by documenting emitters)"""
|
||||
return self.parsers[0]
|
||||
|
||||
|
||||
def get(self, request, auth, *args, **kwargs):
|
||||
"""Must be subclassed to be implemented."""
|
||||
self.not_implemented('GET')
|
||||
|
@ -171,17 +160,6 @@ class Resource(object):
|
|||
pass
|
||||
|
||||
return self.request.build_absolute_uri(path)
|
||||
|
||||
|
||||
def determine_method(self, request):
|
||||
"""Determine the HTTP method that this request should be treated as.
|
||||
Allows PUT and DELETE tunneling via the _method parameter if METHOD_PARAM is set."""
|
||||
method = request.method.upper()
|
||||
|
||||
if method == 'POST' and self.METHOD_PARAM and request.POST.has_key(self.METHOD_PARAM):
|
||||
method = request.POST[self.METHOD_PARAM].upper()
|
||||
|
||||
return method
|
||||
|
||||
|
||||
def authenticate(self, request):
|
||||
|
@ -214,58 +192,6 @@ class Resource(object):
|
|||
{'detail': 'You do not have permission to access this resource. ' +
|
||||
'You may need to login or otherwise authenticate the request.'})
|
||||
|
||||
def get_form(self, data=None):
|
||||
"""Optionally return a Django Form instance, which may be used for validation
|
||||
and/or rendered by an HTML/XHTML emitter.
|
||||
|
||||
If data is not None the form will be bound to data."""
|
||||
|
||||
if self.form:
|
||||
if data:
|
||||
return self.form(data)
|
||||
else:
|
||||
return self.form()
|
||||
return None
|
||||
|
||||
|
||||
def cleanup_request(self, data, form_instance):
|
||||
"""Perform any resource-specific data deserialization and/or validation
|
||||
after the initial HTTP content-type deserialization has taken place.
|
||||
|
||||
Returns a tuple containing the cleaned up data, and optionally a form bound to that data.
|
||||
|
||||
By default this uses form validation to filter the basic input into the required types."""
|
||||
|
||||
if form_instance is None:
|
||||
return data
|
||||
|
||||
# Default form validation does not check for additional invalid fields
|
||||
non_existent_fields = []
|
||||
for key in set(data.keys()) - set(form_instance.fields.keys()):
|
||||
non_existent_fields.append(key)
|
||||
|
||||
if not form_instance.is_valid() or non_existent_fields:
|
||||
if not form_instance.errors and not non_existent_fields:
|
||||
# If no data was supplied the errors property will be None
|
||||
details = 'No content was supplied'
|
||||
|
||||
else:
|
||||
# Add standard field errors
|
||||
details = dict((key, map(unicode, val)) for (key, val) in form_instance.errors.iteritems() if key != '__all__')
|
||||
|
||||
# Add any non-field errors
|
||||
if form_instance.non_field_errors():
|
||||
details['errors'] = form_instance.non_field_errors()
|
||||
|
||||
# Add any non-existent field errors
|
||||
for key in non_existent_fields:
|
||||
details[key] = ['This field does not exist']
|
||||
|
||||
# Bail. Note that we will still serialize this response with the appropriate content type
|
||||
raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': details})
|
||||
|
||||
return form_instance.cleaned_data
|
||||
|
||||
|
||||
def cleanup_response(self, data):
|
||||
"""Perform any resource-specific data filtering prior to the standard HTTP
|
||||
|
@ -275,37 +201,6 @@ class Resource(object):
|
|||
return data
|
||||
|
||||
|
||||
def determine_parser(self, request):
|
||||
"""Return the appropriate parser for the input, given the client's 'Content-Type' header,
|
||||
and the content types that this Resource knows how to parse."""
|
||||
content_type = request.META.get('CONTENT_TYPE', 'application/x-www-form-urlencoded')
|
||||
raw_content = request.raw_post_data
|
||||
|
||||
split = content_type.split(';', 1)
|
||||
if len(split) > 1:
|
||||
content_type = split[0]
|
||||
content_type = content_type.strip()
|
||||
|
||||
# If CONTENTTYPE_PARAM is turned on, and this is a standard POST form then allow the content type to be overridden
|
||||
if (content_type == 'application/x-www-form-urlencoded' and
|
||||
request.method == 'POST' and
|
||||
self.CONTENTTYPE_PARAM and
|
||||
self.CONTENT_PARAM and
|
||||
request.POST.get(self.CONTENTTYPE_PARAM, None) and
|
||||
request.POST.get(self.CONTENT_PARAM, None)):
|
||||
raw_content = request.POST[self.CONTENT_PARAM]
|
||||
content_type = request.POST[self.CONTENTTYPE_PARAM]
|
||||
|
||||
# Create a list of list of (media_type, Parser) tuples
|
||||
media_type_to_parser = dict([(parser.media_type, parser) for parser in self.parsers])
|
||||
|
||||
try:
|
||||
return (media_type_to_parser[content_type], raw_content)
|
||||
except KeyError:
|
||||
raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
||||
{'detail': 'Unsupported media type \'%s\'' % content_type})
|
||||
|
||||
|
||||
def determine_emitter(self, request):
|
||||
"""Return the appropriate emitter for the output, given the client's 'Accept' header,
|
||||
and the content types that this Resource knows how to serve.
|
||||
|
@ -407,11 +302,10 @@ class Resource(object):
|
|||
# Either generate the response data, deserializing and validating any request data
|
||||
# TODO: Add support for message bodys on other HTTP methods, as it is valid.
|
||||
if method in ('PUT', 'POST'):
|
||||
(parser, raw_content) = self.determine_parser(request)
|
||||
data = parser(self).parse(raw_content)
|
||||
self.form_instance = self.get_form(data)
|
||||
data = self.cleanup_request(data, self.form_instance)
|
||||
response = func(request, auth_context, data, *args, **kwargs)
|
||||
(content_type, content) = self.determine_content(request)
|
||||
parser_content = self.parse(content_type, content)
|
||||
cleaned_content = self.validate(parser_content)
|
||||
response = func(request, auth_context, cleaned_content, *args, **kwargs)
|
||||
|
||||
else:
|
||||
response = func(request, auth_context, *args, **kwargs)
|
||||
|
|
|
@ -21,8 +21,8 @@ class TestContentMixins(TestCase):
|
|||
def test_overloaded_content_mixin_interface(self):
|
||||
"""Ensure the OverloadedContentMixin interface is as expected."""
|
||||
self.assertTrue(issubclass(OverloadedContentMixin, ContentMixin))
|
||||
getattr(OverloadedContentMixin, 'FORM_PARAM_CONTENT')
|
||||
getattr(OverloadedContentMixin, 'FORM_PARAM_CONTENTTYPE')
|
||||
getattr(OverloadedContentMixin, 'CONTENT_PARAM')
|
||||
getattr(OverloadedContentMixin, 'CONTENTTYPE_PARAM')
|
||||
getattr(OverloadedContentMixin, 'determine_content')
|
||||
|
||||
|
||||
|
@ -107,14 +107,14 @@ class TestContentMixins(TestCase):
|
|||
"""Ensure determine_content(request) returns (content type, content) for overloaded POST request"""
|
||||
content = 'qwerty'
|
||||
content_type = 'text/plain'
|
||||
form_data = {OverloadedContentMixin.FORM_PARAM_CONTENT: content,
|
||||
OverloadedContentMixin.FORM_PARAM_CONTENTTYPE: content_type}
|
||||
form_data = {OverloadedContentMixin.CONTENT_PARAM: content,
|
||||
OverloadedContentMixin.CONTENTTYPE_PARAM: content_type}
|
||||
request = self.req.post('/', form_data)
|
||||
self.assertEqual(OverloadedContentMixin().determine_content(request), (content_type, content))
|
||||
|
||||
def test_overloaded_behaviour_allows_content_tunnelling_content_type_not_set(self):
|
||||
"""Ensure determine_content(request) returns (None, content) for overloaded POST request with content type not set"""
|
||||
content = 'qwerty'
|
||||
request = self.req.post('/', {OverloadedContentMixin.FORM_PARAM_CONTENT: content})
|
||||
request = self.req.post('/', {OverloadedContentMixin.CONTENT_PARAM: content})
|
||||
self.assertEqual(OverloadedContentMixin().determine_content(request), (None, content))
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ class TestMethodMixins(TestCase):
|
|||
def test_overloaded_method_mixin_interface(self):
|
||||
"""Ensure the OverloadedPOSTMethodMixin interface is as expected."""
|
||||
self.assertTrue(issubclass(OverloadedPOSTMethodMixin, MethodMixin))
|
||||
getattr(OverloadedPOSTMethodMixin, 'FORM_PARAM_METHOD')
|
||||
getattr(OverloadedPOSTMethodMixin, 'METHOD_PARAM')
|
||||
getattr(OverloadedPOSTMethodMixin, 'determine_method')
|
||||
|
||||
# Behavioural tests
|
||||
|
@ -48,5 +48,5 @@ class TestMethodMixins(TestCase):
|
|||
|
||||
def test_overloaded_POST_behaviour_determines_overloaded_method(self):
|
||||
"""POST requests can be overloaded to another method by setting a reserved form field with OverloadedPOSTMethodMixin"""
|
||||
request = self.req.post('/', {OverloadedPOSTMethodMixin.FORM_PARAM_METHOD: 'DELETE'})
|
||||
request = self.req.post('/', {OverloadedPOSTMethodMixin.METHOD_PARAM: 'DELETE'})
|
||||
self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'DELETE')
|
||||
|
|
153
djangorestframework/validators.py
Normal file
153
djangorestframework/validators.py
Normal file
|
@ -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
|
Loading…
Reference in New Issue
Block a user