mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-25 19:14:01 +03:00
refactoring resource specfic stuff into ResourceMixin - validators now defunct
This commit is contained in:
parent
4d12679675
commit
15f9e7c566
|
@ -85,9 +85,9 @@ class UserLoggedInAuthenticaton(BaseAuthenticaton):
|
|||
if getattr(request, 'user', None) and request.user.is_active:
|
||||
# If this is a POST request we enforce CSRF validation.
|
||||
if request.method.upper() == 'POST':
|
||||
# Temporarily replace request.POST with .RAW_CONTENT,
|
||||
# Temporarily replace request.POST with .DATA,
|
||||
# so that we use our more generic request parsing
|
||||
request._post = self.view.RAW_CONTENT
|
||||
request._post = self.view.DATA
|
||||
resp = CsrfViewMiddleware().process_view(request, None, (), {})
|
||||
del(request._post)
|
||||
if resp is not None: # csrf failed
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
""""""
|
||||
"""
|
||||
The mixins module provides a set of reusable mixin classes that can be added to a ``View``.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.db.models.query import QuerySet
|
||||
|
@ -18,9 +20,12 @@ from StringIO import StringIO
|
|||
|
||||
|
||||
__all__ = (
|
||||
# Base behavior mixins
|
||||
'RequestMixin',
|
||||
'ResponseMixin',
|
||||
'AuthMixin',
|
||||
'ResourceMixin',
|
||||
# Model behavior mixins
|
||||
'ReadModelMixin',
|
||||
'CreateModelMixin',
|
||||
'UpdateModelMixin',
|
||||
|
@ -36,13 +41,12 @@ class RequestMixin(object):
|
|||
Mixin class to provide request parsing behavior.
|
||||
"""
|
||||
|
||||
USE_FORM_OVERLOADING = True
|
||||
METHOD_PARAM = "_method"
|
||||
CONTENTTYPE_PARAM = "_content_type"
|
||||
CONTENT_PARAM = "_content"
|
||||
_USE_FORM_OVERLOADING = True
|
||||
_METHOD_PARAM = '_method'
|
||||
_CONTENTTYPE_PARAM = '_content_type'
|
||||
_CONTENT_PARAM = '_content'
|
||||
|
||||
parsers = ()
|
||||
validators = ()
|
||||
|
||||
def _get_method(self):
|
||||
"""
|
||||
|
@ -137,62 +141,58 @@ class RequestMixin(object):
|
|||
self._stream = stream
|
||||
|
||||
|
||||
def _get_raw_content(self):
|
||||
"""
|
||||
Returns the parsed content of the request
|
||||
"""
|
||||
if not hasattr(self, '_raw_content'):
|
||||
self._raw_content = self.parse(self.stream, self.content_type)
|
||||
return self._raw_content
|
||||
def _load_data_and_files(self):
|
||||
(self._data, self._files) = self._parse(self.stream, self.content_type)
|
||||
|
||||
def _get_data(self):
|
||||
if not hasattr(self, '_data'):
|
||||
self._load_data_and_files()
|
||||
return self._data
|
||||
|
||||
def _get_content(self):
|
||||
"""
|
||||
Returns the parsed and validated content of the request
|
||||
"""
|
||||
if not hasattr(self, '_content'):
|
||||
self._content = self.validate(self.RAW_CONTENT)
|
||||
def _get_files(self):
|
||||
if not hasattr(self, '_files'):
|
||||
self._load_data_and_files()
|
||||
return self._files
|
||||
|
||||
return self._content
|
||||
|
||||
# TODO: Modify this so that it happens implictly, rather than being called explicitly
|
||||
# ie accessing any of .DATA, .FILES, .content_type, .stream or .method will force
|
||||
# ie accessing any of .DATA, .FILES, .content_type, .method will force
|
||||
# form overloading.
|
||||
def perform_form_overloading(self):
|
||||
def _perform_form_overloading(self):
|
||||
"""
|
||||
Check the request to see if it is using form POST '_method'/'_content'/'_content_type' overrides.
|
||||
If it is then alter self.method, self.content_type, self.CONTENT to reflect that rather than simply
|
||||
delegating them to the original request.
|
||||
"""
|
||||
if not self.USE_FORM_OVERLOADING or self.method != 'POST' or not is_form_media_type(self.content_type):
|
||||
if not self._USE_FORM_OVERLOADING or self.method != 'POST' or not is_form_media_type(self.content_type):
|
||||
return
|
||||
|
||||
# Temporarily switch to using the form parsers, then parse the content
|
||||
parsers = self.parsers
|
||||
self.parsers = (FormParser, MultiPartParser)
|
||||
content = self.RAW_CONTENT
|
||||
content = self.DATA
|
||||
self.parsers = parsers
|
||||
|
||||
# Method overloading - change the method and remove the param from the content
|
||||
if self.METHOD_PARAM in content:
|
||||
self.method = content[self.METHOD_PARAM].upper()
|
||||
del self._raw_content[self.METHOD_PARAM]
|
||||
if self._METHOD_PARAM in content:
|
||||
self.method = content[self._METHOD_PARAM].upper()
|
||||
del self._data[self._METHOD_PARAM]
|
||||
|
||||
# Content overloading - rewind the stream and modify the content type
|
||||
if self.CONTENT_PARAM in content and self.CONTENTTYPE_PARAM in content:
|
||||
self._content_type = content[self.CONTENTTYPE_PARAM]
|
||||
self._stream = StringIO(content[self.CONTENT_PARAM])
|
||||
del(self._raw_content)
|
||||
if self._CONTENT_PARAM in content and self._CONTENTTYPE_PARAM in content:
|
||||
self._content_type = content[self._CONTENTTYPE_PARAM]
|
||||
self._stream = StringIO(content[self._CONTENT_PARAM])
|
||||
del(self._data)
|
||||
|
||||
|
||||
def parse(self, stream, content_type):
|
||||
def _parse(self, stream, content_type):
|
||||
"""
|
||||
Parse the request content.
|
||||
|
||||
May raise a 415 ErrorResponse (Unsupported Media Type), or a 400 ErrorResponse (Bad Request).
|
||||
"""
|
||||
if stream is None or content_type is None:
|
||||
return None
|
||||
return (None, None)
|
||||
|
||||
parsers = as_tuple(self.parsers)
|
||||
|
||||
|
@ -206,48 +206,28 @@ class RequestMixin(object):
|
|||
content_type})
|
||||
|
||||
|
||||
# TODO: Acutally this needs to go into Resource
|
||||
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
|
||||
|
||||
|
||||
# TODO: Acutally this needs to go into Resource
|
||||
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
|
||||
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.
|
||||
"""
|
||||
return [parser.media_type for parser in self.parsers]
|
||||
|
||||
|
||||
@property
|
||||
def default_parser(self):
|
||||
"""Return the view's most preferred parser.
|
||||
(This has no behavioral effect, but is may be used by documenting renderers)"""
|
||||
"""
|
||||
Return the view's most preferred parser.
|
||||
(This has no behavioral effect, but is may be used by documenting renderers)
|
||||
"""
|
||||
return self.parsers[0]
|
||||
|
||||
|
||||
method = property(_get_method, _set_method)
|
||||
content_type = property(_get_content_type, _set_content_type)
|
||||
stream = property(_get_stream, _set_stream)
|
||||
RAW_CONTENT = property(_get_raw_content)
|
||||
CONTENT = property(_get_content)
|
||||
DATA = property(_get_data)
|
||||
FILES = property(_get_files)
|
||||
|
||||
|
||||
########## ResponseMixin ##########
|
||||
|
@ -422,6 +402,28 @@ class AuthMixin(object):
|
|||
permission.check_permission(user)
|
||||
|
||||
|
||||
########## Resource Mixin ##########
|
||||
|
||||
class ResourceMixin(object):
|
||||
@property
|
||||
def CONTENT(self):
|
||||
if not hasattr(self, '_content'):
|
||||
self._content = self._get_content(self.DATA, self.FILES)
|
||||
return self._content
|
||||
|
||||
def _get_content(self, data, files):
|
||||
resource = self.resource(self)
|
||||
return resource.validate(data, files)
|
||||
|
||||
def get_bound_form(self, content=None):
|
||||
resource = self.resource(self)
|
||||
return resource.get_bound_form(content)
|
||||
|
||||
def object_to_data(self, obj):
|
||||
resource = self.resource(self)
|
||||
return resource.object_to_data(obj)
|
||||
|
||||
|
||||
########## Model Mixins ##########
|
||||
|
||||
class ReadModelMixin(object):
|
||||
|
|
|
@ -41,7 +41,7 @@ class BaseParser(object):
|
|||
"""
|
||||
self.view = view
|
||||
|
||||
def can_handle_request(self, media_type):
|
||||
def can_handle_request(self, content_type):
|
||||
"""
|
||||
Returns `True` if this parser is able to deal with the given media type.
|
||||
|
||||
|
@ -52,12 +52,12 @@ class BaseParser(object):
|
|||
This may be overridden to provide for other behavior, but typically you'll
|
||||
instead want to just set the ``media_type`` attribute on the class.
|
||||
"""
|
||||
return media_type_matches(media_type, self.media_type)
|
||||
return media_type_matches(content_type, self.media_type)
|
||||
|
||||
def parse(self, stream):
|
||||
"""
|
||||
Given a stream to read from, return the deserialized output.
|
||||
The return value may be of any type, but for many parsers it might typically be a dict-like object.
|
||||
Should return a 2-tuple of (data, files).
|
||||
"""
|
||||
raise NotImplementedError("BaseParser.parse() Must be overridden to be implemented.")
|
||||
|
||||
|
@ -67,7 +67,7 @@ class JSONParser(BaseParser):
|
|||
|
||||
def parse(self, stream):
|
||||
try:
|
||||
return json.load(stream)
|
||||
return (json.load(stream), None)
|
||||
except ValueError, exc:
|
||||
raise ErrorResponse(status.HTTP_400_BAD_REQUEST,
|
||||
{'detail': 'JSON parse error - %s' % unicode(exc)})
|
||||
|
@ -107,7 +107,7 @@ class PlainTextParser(BaseParser):
|
|||
media_type = 'text/plain'
|
||||
|
||||
def parse(self, stream):
|
||||
return stream.read()
|
||||
return (stream.read(), None)
|
||||
|
||||
|
||||
class FormParser(BaseParser, DataFlatener):
|
||||
|
@ -139,7 +139,7 @@ class FormParser(BaseParser, DataFlatener):
|
|||
if key in self.RESERVED_FORM_PARAMS:
|
||||
data.pop(key)
|
||||
|
||||
return data
|
||||
return (data, None)
|
||||
|
||||
def remove_empty_val(self, val_list):
|
||||
""" """
|
||||
|
@ -152,11 +152,6 @@ class FormParser(BaseParser, DataFlatener):
|
|||
val_list.pop(ind)
|
||||
|
||||
|
||||
class MultipartData(dict):
|
||||
def __init__(self, data, files):
|
||||
dict.__init__(self, data)
|
||||
self.FILES = files
|
||||
|
||||
class MultiPartParser(BaseParser, DataFlatener):
|
||||
media_type = 'multipart/form-data'
|
||||
RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',)
|
||||
|
@ -175,4 +170,4 @@ class MultiPartParser(BaseParser, DataFlatener):
|
|||
if key in self.RESERVED_FORM_PARAMS:
|
||||
data.pop(key)
|
||||
|
||||
return MultipartData(data, files)
|
||||
return (data, files)
|
||||
|
|
|
@ -150,7 +150,7 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
|||
|
||||
# If we're not using content overloading there's no point in supplying a generic form,
|
||||
# as the view won't treat the form's value as the content of the request.
|
||||
if not getattr(view, 'USE_FORM_OVERLOADING', False):
|
||||
if not getattr(view, '_USE_FORM_OVERLOADING', False):
|
||||
return None
|
||||
|
||||
# NB. http://jacobian.org/writing/dynamic-form-generation/
|
||||
|
@ -164,14 +164,14 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
|||
contenttype_choices = [(media_type, media_type) for media_type in view.parsed_media_types]
|
||||
initial_contenttype = view.default_parser.media_type
|
||||
|
||||
self.fields[view.CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
|
||||
choices=contenttype_choices,
|
||||
initial=initial_contenttype)
|
||||
self.fields[view.CONTENT_PARAM] = forms.CharField(label='Content',
|
||||
widget=forms.Textarea)
|
||||
self.fields[view._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
|
||||
choices=contenttype_choices,
|
||||
initial=initial_contenttype)
|
||||
self.fields[view._CONTENT_PARAM] = forms.CharField(label='Content',
|
||||
widget=forms.Textarea)
|
||||
|
||||
# If either of these reserved parameters are turned off then content tunneling is not possible
|
||||
if self.view.CONTENTTYPE_PARAM is None or self.view.CONTENT_PARAM is None:
|
||||
if self.view._CONTENTTYPE_PARAM is None or self.view._CONTENT_PARAM is None:
|
||||
return None
|
||||
|
||||
# Okey doke, let's do it
|
||||
|
|
|
@ -42,13 +42,13 @@ def _object_to_data(obj):
|
|||
return [_object_to_data(item) for item in obj]
|
||||
if isinstance(obj, models.Manager):
|
||||
# Manager objects
|
||||
ret = [_object_to_data(item) for item in obj.all()]
|
||||
return [_object_to_data(item) for item in obj.all()]
|
||||
if isinstance(obj, models.Model):
|
||||
# Model instances
|
||||
return _object_to_data(_model_to_dict(obj))
|
||||
if isinstance(obj, decimal.Decimal):
|
||||
# Decimals (force to string representation)
|
||||
return str(obj)
|
||||
return str(obj)
|
||||
if inspect.isfunction(obj) and not inspect.getargspec(obj)[0]:
|
||||
# function with no args
|
||||
return _object_to_data(obj())
|
||||
|
@ -60,26 +60,48 @@ def _object_to_data(obj):
|
|||
return smart_unicode(obj, strings_only=True)
|
||||
|
||||
|
||||
# TODO: Replace this with new Serializer code based on Forms API.
|
||||
|
||||
#class Resource(object):
|
||||
# def __init__(self, view):
|
||||
# self.view = view
|
||||
#
|
||||
# def object_to_data(self, obj):
|
||||
# pass
|
||||
#
|
||||
# def data_to_object(self, data, files):
|
||||
# pass
|
||||
#
|
||||
#class FormResource(object):
|
||||
# pass
|
||||
#
|
||||
#class ModelResource(object):
|
||||
# pass
|
||||
def _form_to_data(form):
|
||||
"""
|
||||
Returns a dict containing the data in a form instance.
|
||||
|
||||
This code is pretty much a clone of the ``Form.as_p()`` ``Form.as_ul``
|
||||
and ``Form.as_table()`` methods, except that it returns data suitable
|
||||
for arbitrary serialization, rather than rendering the result directly
|
||||
into html.
|
||||
"""
|
||||
ret = {}
|
||||
for name, field in form.fields.items():
|
||||
if not form.is_bound:
|
||||
data = form.initial.get(name, field.initial)
|
||||
if callable(data):
|
||||
data = data()
|
||||
else:
|
||||
if isinstance(field, FileField) and form.data is None:
|
||||
data = form.initial.get(name, field.initial)
|
||||
else:
|
||||
data = field.widget.value_from_datadict(form.data, form.files, name)
|
||||
ret[name] = field.prepare_value(data)
|
||||
return ret
|
||||
|
||||
|
||||
class Resource(object):
|
||||
class BaseResource(object):
|
||||
"""Base class for all Resource classes, which simply defines the interface they provide."""
|
||||
|
||||
def __init__(self, view):
|
||||
self.view = view
|
||||
|
||||
def validate(self, data, files):
|
||||
"""Given some content as input return some cleaned, validated content.
|
||||
Typically raises a ErrorResponse with status code 400 (Bad Request) on failure.
|
||||
|
||||
Must be overridden to be implemented."""
|
||||
return data
|
||||
|
||||
def object_to_data(self, obj):
|
||||
return _object_to_data(obj)
|
||||
|
||||
|
||||
class Resource(BaseResource):
|
||||
"""
|
||||
A Resource determines how a python object maps to some serializable data.
|
||||
Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets.
|
||||
|
@ -99,9 +121,11 @@ class Resource(object):
|
|||
# you should explicitly set the fields attribute on your class.
|
||||
fields = None
|
||||
|
||||
@classmethod
|
||||
def object_to_serializable(self, data):
|
||||
"""A (horrible) munging of Piston's pre-serialization. Returns a dict"""
|
||||
# TODO: Replace this with new Serializer code based on Forms API.
|
||||
def object_to_data(self, obj):
|
||||
"""
|
||||
A (horrible) munging of Piston's pre-serialization. Returns a dict.
|
||||
"""
|
||||
|
||||
def _any(thing, fields=()):
|
||||
"""
|
||||
|
@ -321,5 +345,208 @@ class Resource(object):
|
|||
return dict([ (k, _any(v)) for k, v in data.iteritems() ])
|
||||
|
||||
# Kickstart the seralizin'.
|
||||
return _any(data, self.fields)
|
||||
return _any(obj, self.fields)
|
||||
|
||||
|
||||
class FormResource(Resource):
|
||||
"""Validator class that uses forms for validation.
|
||||
Also provides a get_bound_form() method which may be used by some renderers.
|
||||
|
||||
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."""
|
||||
|
||||
|
||||
def validate(self, data, files):
|
||||
"""
|
||||
Given some content as input return some cleaned, validated content.
|
||||
Raises a ErrorResponse 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 ErrorResponse 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(data, files)
|
||||
|
||||
|
||||
def _validate(self, data, files, allowed_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.
|
||||
"""
|
||||
bound_form = self.get_bound_form(data, files)
|
||||
|
||||
if bound_form is None:
|
||||
return data
|
||||
|
||||
self.view.bound_form_instance = bound_form
|
||||
|
||||
seen_fields_set = set(data.keys())
|
||||
form_fields_set = set(bound_form.fields.keys())
|
||||
allowed_extra_fields_set = set(allowed_extra_fields)
|
||||
|
||||
# In addition to regular validation we also ensure no additional fields are being passed in...
|
||||
unknown_fields = seen_fields_set - (form_fields_set | allowed_extra_fields_set)
|
||||
|
||||
# Check using both regular validation, and our stricter no additional fields rule
|
||||
if bound_form.is_valid() and not unknown_fields:
|
||||
# Validation succeeded...
|
||||
cleaned_data = bound_form.cleaned_data
|
||||
|
||||
cleaned_data.update(bound_form.files)
|
||||
|
||||
# Add in any extra fields to the cleaned content...
|
||||
for key in (allowed_extra_fields_set & seen_fields_set) - set(cleaned_data.keys()):
|
||||
cleaned_data[key] = data[key]
|
||||
|
||||
return cleaned_data
|
||||
|
||||
# Validation failed...
|
||||
detail = {}
|
||||
|
||||
if not bound_form.errors and not unknown_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.']
|
||||
|
||||
if field_errors:
|
||||
detail[u'field-errors'] = field_errors
|
||||
|
||||
# Return HTTP 400 response (BAD REQUEST)
|
||||
raise ErrorResponse(400, detail)
|
||||
|
||||
|
||||
def get_bound_form(self, data=None, files=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."""
|
||||
form_cls = getattr(self, 'form', None)
|
||||
|
||||
if not form_cls:
|
||||
return None
|
||||
|
||||
if data is not None:
|
||||
return form_cls(data, files)
|
||||
|
||||
return form_cls()
|
||||
|
||||
|
||||
class ModelResource(FormResource):
|
||||
"""Validator class that uses forms for validation and otherwise falls back to a model form if no form is set.
|
||||
Also provides a get_bound_form() method which may be used by some renderers."""
|
||||
|
||||
"""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.
|
||||
|
||||
Setting the fields class attribute causes the exclude_fields class attribute to be disregarded."""
|
||||
fields = None
|
||||
|
||||
"""The list of fields to exclude from the Model. This is only used if the fields class attribute is not set."""
|
||||
exclude_fields = ('id', 'pk')
|
||||
|
||||
|
||||
# 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, data, files):
|
||||
"""
|
||||
Given some content as input return some cleaned, validated content.
|
||||
Raises a ErrorResponse 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 ErrorResponse 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(data, files, allowed_extra_fields=self._property_fields_set)
|
||||
|
||||
|
||||
def get_bound_form(self, data=None, files=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."""
|
||||
|
||||
form_cls = getattr(self, 'form', None)
|
||||
model_cls = getattr(self, 'model', None)
|
||||
|
||||
if form_cls:
|
||||
# Use explict Form
|
||||
return super(ModelFormValidator, self).get_bound_form(data, files)
|
||||
|
||||
elif model_cls:
|
||||
# Fall back to ModelForm which we create on the fly
|
||||
class OnTheFlyModelForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = model_cls
|
||||
#fields = tuple(self._model_fields_set)
|
||||
|
||||
# Instantiate the ModelForm as appropriate
|
||||
if content and isinstance(content, models.Model):
|
||||
# Bound to an existing model instance
|
||||
return OnTheFlyModelForm(instance=content)
|
||||
elif not data is None:
|
||||
return OnTheFlyModelForm(data, files)
|
||||
return OnTheFlyModelForm()
|
||||
|
||||
# Both form and model not set? Okay bruv, whatevs...
|
||||
return None
|
||||
|
||||
|
||||
@property
|
||||
def _model_fields_set(self):
|
||||
"""Return a set containing the names of validated fields on the model."""
|
||||
resource = self.view.resource
|
||||
model = getattr(resource, 'model', None)
|
||||
fields = getattr(resource, 'fields', self.fields)
|
||||
exclude_fields = getattr(resource, 'exclude_fields', self.exclude_fields)
|
||||
|
||||
model_fields = set(field.name for field in model._meta.fields)
|
||||
|
||||
if fields:
|
||||
return model_fields & set(as_tuple(fields))
|
||||
|
||||
return model_fields - set(as_tuple(exclude_fields))
|
||||
|
||||
@property
|
||||
def _property_fields_set(self):
|
||||
"""Returns a set containing the names of validated properties on the model."""
|
||||
resource = self.view.resource
|
||||
model = getattr(resource, 'model', None)
|
||||
fields = getattr(resource, 'fields', self.fields)
|
||||
exclude_fields = getattr(resource, '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('_'))
|
||||
|
||||
if fields:
|
||||
return property_fields & set(as_tuple(fields))
|
||||
|
||||
return property_fields - set(as_tuple(exclude_fields))
|
||||
|
|
|
@ -14,14 +14,14 @@ class TestContentParsing(TestCase):
|
|||
def ensure_determines_no_content_GET(self, view):
|
||||
"""Ensure view.RAW_CONTENT returns None for GET request with no content."""
|
||||
view.request = self.req.get('/')
|
||||
self.assertEqual(view.RAW_CONTENT, None)
|
||||
self.assertEqual(view.DATA, None)
|
||||
|
||||
def ensure_determines_form_content_POST(self, view):
|
||||
"""Ensure view.RAW_CONTENT returns content for POST request with form content."""
|
||||
form_data = {'qwerty': 'uiop'}
|
||||
view.parsers = (FormParser, MultiPartParser)
|
||||
view.request = self.req.post('/', data=form_data)
|
||||
self.assertEqual(view.RAW_CONTENT, form_data)
|
||||
self.assertEqual(view.DATA, form_data)
|
||||
|
||||
def ensure_determines_non_form_content_POST(self, view):
|
||||
"""Ensure view.RAW_CONTENT returns content for POST request with non-form content."""
|
||||
|
@ -29,14 +29,14 @@ class TestContentParsing(TestCase):
|
|||
content_type = 'text/plain'
|
||||
view.parsers = (PlainTextParser,)
|
||||
view.request = self.req.post('/', content, content_type=content_type)
|
||||
self.assertEqual(view.RAW_CONTENT, content)
|
||||
self.assertEqual(view.DATA, content)
|
||||
|
||||
def ensure_determines_form_content_PUT(self, view):
|
||||
"""Ensure view.RAW_CONTENT returns content for PUT request with form content."""
|
||||
form_data = {'qwerty': 'uiop'}
|
||||
view.parsers = (FormParser, MultiPartParser)
|
||||
view.request = self.req.put('/', data=form_data)
|
||||
self.assertEqual(view.RAW_CONTENT, form_data)
|
||||
self.assertEqual(view.DATA, form_data)
|
||||
|
||||
def ensure_determines_non_form_content_PUT(self, view):
|
||||
"""Ensure view.RAW_CONTENT returns content for PUT request with non-form content."""
|
||||
|
@ -44,36 +44,36 @@ class TestContentParsing(TestCase):
|
|||
content_type = 'text/plain'
|
||||
view.parsers = (PlainTextParser,)
|
||||
view.request = self.req.post('/', content, content_type=content_type)
|
||||
self.assertEqual(view.RAW_CONTENT, content)
|
||||
self.assertEqual(view.DATA, content)
|
||||
|
||||
def test_standard_behaviour_determines_no_content_GET(self):
|
||||
"""Ensure view.RAW_CONTENT returns None for GET request with no content."""
|
||||
"""Ensure view.DATA returns None for GET request with no content."""
|
||||
self.ensure_determines_no_content_GET(RequestMixin())
|
||||
|
||||
def test_standard_behaviour_determines_form_content_POST(self):
|
||||
"""Ensure view.RAW_CONTENT returns content for POST request with form content."""
|
||||
"""Ensure view.DATA returns content for POST request with form content."""
|
||||
self.ensure_determines_form_content_POST(RequestMixin())
|
||||
|
||||
def test_standard_behaviour_determines_non_form_content_POST(self):
|
||||
"""Ensure view.RAW_CONTENT returns content for POST request with non-form content."""
|
||||
"""Ensure view.DATA returns content for POST request with non-form content."""
|
||||
self.ensure_determines_non_form_content_POST(RequestMixin())
|
||||
|
||||
def test_standard_behaviour_determines_form_content_PUT(self):
|
||||
"""Ensure view.RAW_CONTENT returns content for PUT request with form content."""
|
||||
"""Ensure view.DATA returns content for PUT request with form content."""
|
||||
self.ensure_determines_form_content_PUT(RequestMixin())
|
||||
|
||||
def test_standard_behaviour_determines_non_form_content_PUT(self):
|
||||
"""Ensure view.RAW_CONTENT returns content for PUT request with non-form content."""
|
||||
"""Ensure view.DATA returns content for PUT request with non-form content."""
|
||||
self.ensure_determines_non_form_content_PUT(RequestMixin())
|
||||
|
||||
def test_overloaded_behaviour_allows_content_tunnelling(self):
|
||||
"""Ensure request.RAW_CONTENT returns content for overloaded POST request"""
|
||||
"""Ensure request.DATA returns content for overloaded POST request"""
|
||||
content = 'qwerty'
|
||||
content_type = 'text/plain'
|
||||
view = RequestMixin()
|
||||
form_data = {view.CONTENT_PARAM: content,
|
||||
view.CONTENTTYPE_PARAM: content_type}
|
||||
form_data = {view._CONTENT_PARAM: content,
|
||||
view._CONTENTTYPE_PARAM: content_type}
|
||||
view.request = self.req.post('/', form_data)
|
||||
view.parsers = (PlainTextParser,)
|
||||
view.perform_form_overloading()
|
||||
self.assertEqual(view.RAW_CONTENT, content)
|
||||
view._perform_form_overloading()
|
||||
self.assertEqual(view.DATA, content)
|
||||
|
|
|
@ -2,6 +2,7 @@ from django.test import TestCase
|
|||
from django import forms
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.views import BaseView
|
||||
from djangorestframework.resource import FormResource
|
||||
import StringIO
|
||||
|
||||
class UploadFilesTests(TestCase):
|
||||
|
@ -15,9 +16,12 @@ class UploadFilesTests(TestCase):
|
|||
class FileForm(forms.Form):
|
||||
file = forms.FileField
|
||||
|
||||
class MockResource(FormResource):
|
||||
form = FileForm
|
||||
|
||||
class MockView(BaseView):
|
||||
permissions = ()
|
||||
form = FileForm
|
||||
resource = MockResource
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
return {'FILE_NAME': self.CONTENT['file'].name,
|
||||
|
|
|
@ -22,6 +22,6 @@ class TestMethodOverloading(TestCase):
|
|||
def test_overloaded_POST_behaviour_determines_overloaded_method(self):
|
||||
"""POST requests can be overloaded to another method by setting a reserved form field"""
|
||||
view = RequestMixin()
|
||||
view.request = self.req.post('/', {view.METHOD_PARAM: 'DELETE'})
|
||||
view.perform_form_overloading()
|
||||
view.request = self.req.post('/', {view._METHOD_PARAM: 'DELETE'})
|
||||
view._perform_form_overloading()
|
||||
self.assertEqual(view.method, 'DELETE')
|
||||
|
|
|
@ -24,7 +24,8 @@ Here is some example data, which would eventually be sent along with a post requ
|
|||
|
||||
Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter :
|
||||
|
||||
>>> FormParser(some_view).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': 'blo1'}
|
||||
>>> (data, files) = FormParser(some_view).parse(StringIO(inpt))
|
||||
>>> data == {'key1': 'bla1', 'key2': 'blo1'}
|
||||
True
|
||||
|
||||
However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` :
|
||||
|
@ -36,7 +37,8 @@ However, you can customize this behaviour by subclassing :class:`parsers.FormPar
|
|||
|
||||
This new parser only flattens the lists of parameters that contain a single value.
|
||||
|
||||
>>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
|
||||
>>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
||||
>>> data == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
|
||||
True
|
||||
|
||||
.. note:: The same functionality is available for :class:`parsers.MultiPartParser`.
|
||||
|
@ -61,7 +63,8 @@ The browsers usually strip the parameter completely. A hack to avoid this, and t
|
|||
|
||||
:class:`parsers.FormParser` strips the values ``_empty`` from all the lists.
|
||||
|
||||
>>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'blo1'}
|
||||
>>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
||||
>>> data == {'key1': 'blo1'}
|
||||
True
|
||||
|
||||
Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it.
|
||||
|
@ -71,7 +74,8 @@ Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a lis
|
|||
... def is_a_list(self, key, val_list):
|
||||
... return key == 'key2'
|
||||
...
|
||||
>>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'blo1', 'key2': []}
|
||||
>>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
||||
>>> data == {'key1': 'blo1', 'key2': []}
|
||||
True
|
||||
|
||||
Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`.
|
||||
|
@ -123,7 +127,7 @@ class TestMultiPartParser(TestCase):
|
|||
post_req = RequestFactory().post('/', self.body, content_type=self.content_type)
|
||||
view = BaseView()
|
||||
view.request = post_req
|
||||
parsed = MultiPartParser(view).parse(StringIO(self.body))
|
||||
self.assertEqual(parsed['key1'], 'val1')
|
||||
self.assertEqual(parsed.FILES['file1'].read(), 'blablabla')
|
||||
(data, files) = MultiPartParser(view).parse(StringIO(self.body))
|
||||
self.assertEqual(data['key1'], 'val1')
|
||||
self.assertEqual(files['file1'].read(), 'blablabla')
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ __all__ = (
|
|||
|
||||
|
||||
|
||||
class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
|
||||
class BaseView(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, View):
|
||||
"""Handles incoming requests and maps them to REST operations.
|
||||
Performs request deserialization, response serialization, authentication and input validation."""
|
||||
|
||||
|
@ -46,9 +46,6 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
|
|||
# List of all permissions that must be checked.
|
||||
permissions = ( permissions.FullAnonAccess, )
|
||||
|
||||
# Optional form for input validation and presentation of HTML formatted responses.
|
||||
form = None
|
||||
|
||||
# Allow name and description for the Resource to be set explicitly,
|
||||
# overiding the default classname/docstring behaviour.
|
||||
# These are used for documentation in the standard html and text renderers.
|
||||
|
@ -60,22 +57,13 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
|
|||
return [method.upper() for method in self.http_method_names if hasattr(self, method)]
|
||||
|
||||
def http_method_not_allowed(self, request, *args, **kwargs):
|
||||
"""Return an HTTP 405 error if an operation is called which does not have a handler method."""
|
||||
"""
|
||||
Return an HTTP 405 error if an operation is called which does not have a handler method.
|
||||
"""
|
||||
raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
|
||||
|
||||
|
||||
def cleanup_response(self, data):
|
||||
"""Perform any resource-specific data filtering prior to the standard HTTP
|
||||
content-type serialization.
|
||||
|
||||
Eg filter complex objects that cannot be serialized by json/xml/etc into basic objects that can.
|
||||
|
||||
TODO: This is going to be removed. I think that the 'fields' behaviour is going to move into
|
||||
the RendererMixin and Renderer classes."""
|
||||
return data
|
||||
|
||||
|
||||
# Note: session based authentication is explicitly CSRF validated,
|
||||
# all other authentication is CSRF exempt.
|
||||
@csrf_exempt
|
||||
|
@ -92,7 +80,7 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
|
|||
try:
|
||||
# If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
|
||||
# self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately.
|
||||
self.perform_form_overloading()
|
||||
self._perform_form_overloading()
|
||||
|
||||
# Authenticate and check request is has the relevant permissions
|
||||
self._check_permissions()
|
||||
|
@ -114,13 +102,14 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
|
|||
response = Response(status.HTTP_204_NO_CONTENT)
|
||||
|
||||
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
|
||||
response.cleaned_content = self.resource.object_to_serializable(response.raw_content)
|
||||
response.cleaned_content = self.object_to_data(response.raw_content)
|
||||
|
||||
except ErrorResponse, exc:
|
||||
response = exc.response
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
# Always add these headers.
|
||||
#
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
"""The root view for the examples provided with Django REST framework"""
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.views import BaseView
|
||||
|
||||
|
||||
class Sandbox(Resource):
|
||||
class Sandbox(BaseView):
|
||||
"""This is the sandbox for the examples provided with [Django REST framework](http://django-rest-framework.org).
|
||||
|
||||
These examples are provided to help you get a better idea of the some of the features of RESTful APIs created using the framework.
|
||||
|
|
Loading…
Reference in New Issue
Block a user