refactoring resource specfic stuff into ResourceMixin - validators now defunct

This commit is contained in:
Tom Christie 2011-05-12 12:55:13 +01:00
parent 4d12679675
commit 15f9e7c566
11 changed files with 373 additions and 152 deletions

View File

@ -85,9 +85,9 @@ class UserLoggedInAuthenticaton(BaseAuthenticaton):
if getattr(request, 'user', None) and request.user.is_active: if getattr(request, 'user', None) and request.user.is_active:
# If this is a POST request we enforce CSRF validation. # If this is a POST request we enforce CSRF validation.
if request.method.upper() == 'POST': 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 # 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, (), {}) resp = CsrfViewMiddleware().process_view(request, None, (), {})
del(request._post) del(request._post)
if resp is not None: # csrf failed if resp is not None: # csrf failed

View File

@ -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.contrib.auth.models import AnonymousUser
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
@ -18,9 +20,12 @@ from StringIO import StringIO
__all__ = ( __all__ = (
# Base behavior mixins
'RequestMixin', 'RequestMixin',
'ResponseMixin', 'ResponseMixin',
'AuthMixin', 'AuthMixin',
'ResourceMixin',
# Model behavior mixins
'ReadModelMixin', 'ReadModelMixin',
'CreateModelMixin', 'CreateModelMixin',
'UpdateModelMixin', 'UpdateModelMixin',
@ -36,13 +41,12 @@ class RequestMixin(object):
Mixin class to provide request parsing behavior. Mixin class to provide request parsing behavior.
""" """
USE_FORM_OVERLOADING = True _USE_FORM_OVERLOADING = True
METHOD_PARAM = "_method" _METHOD_PARAM = '_method'
CONTENTTYPE_PARAM = "_content_type" _CONTENTTYPE_PARAM = '_content_type'
CONTENT_PARAM = "_content" _CONTENT_PARAM = '_content'
parsers = () parsers = ()
validators = ()
def _get_method(self): def _get_method(self):
""" """
@ -137,62 +141,58 @@ class RequestMixin(object):
self._stream = stream self._stream = stream
def _get_raw_content(self): def _load_data_and_files(self):
""" (self._data, self._files) = self._parse(self.stream, self.content_type)
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 _get_data(self):
if not hasattr(self, '_data'):
self._load_data_and_files()
return self._data
def _get_content(self): def _get_files(self):
""" if not hasattr(self, '_files'):
Returns the parsed and validated content of the request self._load_data_and_files()
""" return self._files
if not hasattr(self, '_content'):
self._content = self.validate(self.RAW_CONTENT)
return self._content
# TODO: Modify this so that it happens implictly, rather than being called explicitly # 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. # 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. 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 If it is then alter self.method, self.content_type, self.CONTENT to reflect that rather than simply
delegating them to the original request. 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 return
# Temporarily switch to using the form parsers, then parse the content # Temporarily switch to using the form parsers, then parse the content
parsers = self.parsers parsers = self.parsers
self.parsers = (FormParser, MultiPartParser) self.parsers = (FormParser, MultiPartParser)
content = self.RAW_CONTENT content = self.DATA
self.parsers = parsers self.parsers = parsers
# Method overloading - change the method and remove the param from the content # Method overloading - change the method and remove the param from the content
if self.METHOD_PARAM in content: if self._METHOD_PARAM in content:
self.method = content[self.METHOD_PARAM].upper() self.method = content[self._METHOD_PARAM].upper()
del self._raw_content[self.METHOD_PARAM] del self._data[self._METHOD_PARAM]
# Content overloading - rewind the stream and modify the content type # Content overloading - rewind the stream and modify the content type
if self.CONTENT_PARAM in content and self.CONTENTTYPE_PARAM in content: if self._CONTENT_PARAM in content and self._CONTENTTYPE_PARAM in content:
self._content_type = content[self.CONTENTTYPE_PARAM] self._content_type = content[self._CONTENTTYPE_PARAM]
self._stream = StringIO(content[self.CONTENT_PARAM]) self._stream = StringIO(content[self._CONTENT_PARAM])
del(self._raw_content) del(self._data)
def parse(self, stream, content_type): def _parse(self, stream, content_type):
""" """
Parse the request content. Parse the request content.
May raise a 415 ErrorResponse (Unsupported Media Type), or a 400 ErrorResponse (Bad Request). May raise a 415 ErrorResponse (Unsupported Media Type), or a 400 ErrorResponse (Bad Request).
""" """
if stream is None or content_type is None: if stream is None or content_type is None:
return None return (None, None)
parsers = as_tuple(self.parsers) parsers = as_tuple(self.parsers)
@ -206,48 +206,28 @@ class RequestMixin(object):
content_type}) 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 @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.
"""
return [parser.media_type for parser in self.parsers] return [parser.media_type for parser in self.parsers]
@property @property
def default_parser(self): 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] return self.parsers[0]
method = property(_get_method, _set_method) method = property(_get_method, _set_method)
content_type = property(_get_content_type, _set_content_type) content_type = property(_get_content_type, _set_content_type)
stream = property(_get_stream, _set_stream) stream = property(_get_stream, _set_stream)
RAW_CONTENT = property(_get_raw_content) DATA = property(_get_data)
CONTENT = property(_get_content) FILES = property(_get_files)
########## ResponseMixin ########## ########## ResponseMixin ##########
@ -422,6 +402,28 @@ class AuthMixin(object):
permission.check_permission(user) 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 ########## ########## Model Mixins ##########
class ReadModelMixin(object): class ReadModelMixin(object):

View File

@ -41,7 +41,7 @@ class BaseParser(object):
""" """
self.view = view 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. 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 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. 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): def parse(self, stream):
""" """
Given a stream to read from, return the deserialized output. 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.") raise NotImplementedError("BaseParser.parse() Must be overridden to be implemented.")
@ -67,7 +67,7 @@ class JSONParser(BaseParser):
def parse(self, stream): def parse(self, stream):
try: try:
return json.load(stream) return (json.load(stream), None)
except ValueError, exc: except ValueError, exc:
raise ErrorResponse(status.HTTP_400_BAD_REQUEST, raise ErrorResponse(status.HTTP_400_BAD_REQUEST,
{'detail': 'JSON parse error - %s' % unicode(exc)}) {'detail': 'JSON parse error - %s' % unicode(exc)})
@ -107,7 +107,7 @@ class PlainTextParser(BaseParser):
media_type = 'text/plain' media_type = 'text/plain'
def parse(self, stream): def parse(self, stream):
return stream.read() return (stream.read(), None)
class FormParser(BaseParser, DataFlatener): class FormParser(BaseParser, DataFlatener):
@ -139,7 +139,7 @@ class FormParser(BaseParser, DataFlatener):
if key in self.RESERVED_FORM_PARAMS: if key in self.RESERVED_FORM_PARAMS:
data.pop(key) data.pop(key)
return data return (data, None)
def remove_empty_val(self, val_list): def remove_empty_val(self, val_list):
""" """ """ """
@ -152,11 +152,6 @@ class FormParser(BaseParser, DataFlatener):
val_list.pop(ind) val_list.pop(ind)
class MultipartData(dict):
def __init__(self, data, files):
dict.__init__(self, data)
self.FILES = files
class MultiPartParser(BaseParser, DataFlatener): class MultiPartParser(BaseParser, DataFlatener):
media_type = 'multipart/form-data' media_type = 'multipart/form-data'
RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',) RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',)
@ -175,4 +170,4 @@ class MultiPartParser(BaseParser, DataFlatener):
if key in self.RESERVED_FORM_PARAMS: if key in self.RESERVED_FORM_PARAMS:
data.pop(key) data.pop(key)
return MultipartData(data, files) return (data, files)

View File

@ -150,7 +150,7 @@ class DocumentingTemplateRenderer(BaseRenderer):
# If we're not using content overloading there's no point in supplying a generic form, # 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. # 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 return None
# NB. http://jacobian.org/writing/dynamic-form-generation/ # 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] contenttype_choices = [(media_type, media_type) for media_type in view.parsed_media_types]
initial_contenttype = view.default_parser.media_type initial_contenttype = view.default_parser.media_type
self.fields[view.CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type', self.fields[view._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
choices=contenttype_choices, choices=contenttype_choices,
initial=initial_contenttype) initial=initial_contenttype)
self.fields[view.CONTENT_PARAM] = forms.CharField(label='Content', self.fields[view._CONTENT_PARAM] = forms.CharField(label='Content',
widget=forms.Textarea) widget=forms.Textarea)
# If either of these reserved parameters are turned off then content tunneling is not possible # 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 return None
# Okey doke, let's do it # Okey doke, let's do it

View File

@ -42,13 +42,13 @@ def _object_to_data(obj):
return [_object_to_data(item) for item in obj] return [_object_to_data(item) for item in obj]
if isinstance(obj, models.Manager): if isinstance(obj, models.Manager):
# Manager objects # 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): if isinstance(obj, models.Model):
# Model instances # Model instances
return _object_to_data(_model_to_dict(obj)) return _object_to_data(_model_to_dict(obj))
if isinstance(obj, decimal.Decimal): if isinstance(obj, decimal.Decimal):
# Decimals (force to string representation) # Decimals (force to string representation)
return str(obj) return str(obj)
if inspect.isfunction(obj) and not inspect.getargspec(obj)[0]: if inspect.isfunction(obj) and not inspect.getargspec(obj)[0]:
# function with no args # function with no args
return _object_to_data(obj()) return _object_to_data(obj())
@ -60,26 +60,48 @@ def _object_to_data(obj):
return smart_unicode(obj, strings_only=True) return smart_unicode(obj, strings_only=True)
# TODO: Replace this with new Serializer code based on Forms API. def _form_to_data(form):
"""
Returns a dict containing the data in a form instance.
#class Resource(object): This code is pretty much a clone of the ``Form.as_p()`` ``Form.as_ul``
# def __init__(self, view): and ``Form.as_table()`` methods, except that it returns data suitable
# self.view = view for arbitrary serialization, rather than rendering the result directly
# into html.
# def object_to_data(self, obj): """
# pass ret = {}
# for name, field in form.fields.items():
# def data_to_object(self, data, files): if not form.is_bound:
# pass data = form.initial.get(name, field.initial)
# if callable(data):
#class FormResource(object): data = data()
# pass else:
# if isinstance(field, FileField) and form.data is None:
#class ModelResource(object): data = form.initial.get(name, field.initial)
# pass 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. 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. 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. # you should explicitly set the fields attribute on your class.
fields = None fields = None
@classmethod # TODO: Replace this with new Serializer code based on Forms API.
def object_to_serializable(self, data): def object_to_data(self, obj):
"""A (horrible) munging of Piston's pre-serialization. Returns a dict""" """
A (horrible) munging of Piston's pre-serialization. Returns a dict.
"""
def _any(thing, fields=()): def _any(thing, fields=()):
""" """
@ -321,5 +345,208 @@ class Resource(object):
return dict([ (k, _any(v)) for k, v in data.iteritems() ]) return dict([ (k, _any(v)) for k, v in data.iteritems() ])
# Kickstart the seralizin'. # 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))

View File

@ -14,14 +14,14 @@ class TestContentParsing(TestCase):
def ensure_determines_no_content_GET(self, view): def ensure_determines_no_content_GET(self, view):
"""Ensure view.RAW_CONTENT returns None for GET request with no content.""" """Ensure view.RAW_CONTENT returns None for GET request with no content."""
view.request = self.req.get('/') view.request = self.req.get('/')
self.assertEqual(view.RAW_CONTENT, None) self.assertEqual(view.DATA, None)
def ensure_determines_form_content_POST(self, view): def ensure_determines_form_content_POST(self, view):
"""Ensure view.RAW_CONTENT returns content for POST request with form content.""" """Ensure view.RAW_CONTENT returns content for POST request with form content."""
form_data = {'qwerty': 'uiop'} form_data = {'qwerty': 'uiop'}
view.parsers = (FormParser, MultiPartParser) view.parsers = (FormParser, MultiPartParser)
view.request = self.req.post('/', data=form_data) 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): def ensure_determines_non_form_content_POST(self, view):
"""Ensure view.RAW_CONTENT returns content for POST request with non-form content.""" """Ensure view.RAW_CONTENT returns content for POST request with non-form content."""
@ -29,14 +29,14 @@ class TestContentParsing(TestCase):
content_type = 'text/plain' content_type = 'text/plain'
view.parsers = (PlainTextParser,) view.parsers = (PlainTextParser,)
view.request = self.req.post('/', content, content_type=content_type) 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): def ensure_determines_form_content_PUT(self, view):
"""Ensure view.RAW_CONTENT returns content for PUT request with form content.""" """Ensure view.RAW_CONTENT returns content for PUT request with form content."""
form_data = {'qwerty': 'uiop'} form_data = {'qwerty': 'uiop'}
view.parsers = (FormParser, MultiPartParser) view.parsers = (FormParser, MultiPartParser)
view.request = self.req.put('/', data=form_data) 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): def ensure_determines_non_form_content_PUT(self, view):
"""Ensure view.RAW_CONTENT returns content for PUT request with non-form content.""" """Ensure view.RAW_CONTENT returns content for PUT request with non-form content."""
@ -44,36 +44,36 @@ class TestContentParsing(TestCase):
content_type = 'text/plain' content_type = 'text/plain'
view.parsers = (PlainTextParser,) view.parsers = (PlainTextParser,)
view.request = self.req.post('/', content, content_type=content_type) 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): 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()) self.ensure_determines_no_content_GET(RequestMixin())
def test_standard_behaviour_determines_form_content_POST(self): 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()) self.ensure_determines_form_content_POST(RequestMixin())
def test_standard_behaviour_determines_non_form_content_POST(self): 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()) self.ensure_determines_non_form_content_POST(RequestMixin())
def test_standard_behaviour_determines_form_content_PUT(self): 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()) self.ensure_determines_form_content_PUT(RequestMixin())
def test_standard_behaviour_determines_non_form_content_PUT(self): 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()) self.ensure_determines_non_form_content_PUT(RequestMixin())
def test_overloaded_behaviour_allows_content_tunnelling(self): 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 = 'qwerty'
content_type = 'text/plain' content_type = 'text/plain'
view = RequestMixin() view = RequestMixin()
form_data = {view.CONTENT_PARAM: content, form_data = {view._CONTENT_PARAM: content,
view.CONTENTTYPE_PARAM: content_type} view._CONTENTTYPE_PARAM: content_type}
view.request = self.req.post('/', form_data) view.request = self.req.post('/', form_data)
view.parsers = (PlainTextParser,) view.parsers = (PlainTextParser,)
view.perform_form_overloading() view._perform_form_overloading()
self.assertEqual(view.RAW_CONTENT, content) self.assertEqual(view.DATA, content)

View File

@ -2,6 +2,7 @@ from django.test import TestCase
from django import forms from django import forms
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.views import BaseView from djangorestframework.views import BaseView
from djangorestframework.resource import FormResource
import StringIO import StringIO
class UploadFilesTests(TestCase): class UploadFilesTests(TestCase):
@ -15,9 +16,12 @@ class UploadFilesTests(TestCase):
class FileForm(forms.Form): class FileForm(forms.Form):
file = forms.FileField file = forms.FileField
class MockResource(FormResource):
form = FileForm
class MockView(BaseView): class MockView(BaseView):
permissions = () permissions = ()
form = FileForm resource = MockResource
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
return {'FILE_NAME': self.CONTENT['file'].name, return {'FILE_NAME': self.CONTENT['file'].name,

View File

@ -22,6 +22,6 @@ class TestMethodOverloading(TestCase):
def test_overloaded_POST_behaviour_determines_overloaded_method(self): def test_overloaded_POST_behaviour_determines_overloaded_method(self):
"""POST requests can be overloaded to another method by setting a reserved form field""" """POST requests can be overloaded to another method by setting a reserved form field"""
view = RequestMixin() view = RequestMixin()
view.request = self.req.post('/', {view.METHOD_PARAM: 'DELETE'}) view.request = self.req.post('/', {view._METHOD_PARAM: 'DELETE'})
view.perform_form_overloading() view._perform_form_overloading()
self.assertEqual(view.method, 'DELETE') self.assertEqual(view.method, 'DELETE')

View File

@ -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 : 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 True
However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` : 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. 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 True
.. note:: The same functionality is available for :class:`parsers.MultiPartParser`. .. 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. :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 True
Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it. 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): ... def is_a_list(self, key, val_list):
... return key == 'key2' ... 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 True
Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`. 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) post_req = RequestFactory().post('/', self.body, content_type=self.content_type)
view = BaseView() view = BaseView()
view.request = post_req view.request = post_req
parsed = MultiPartParser(view).parse(StringIO(self.body)) (data, files) = MultiPartParser(view).parse(StringIO(self.body))
self.assertEqual(parsed['key1'], 'val1') self.assertEqual(data['key1'], 'val1')
self.assertEqual(parsed.FILES['file1'].read(), 'blablabla') self.assertEqual(files['file1'].read(), 'blablabla')

View File

@ -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. """Handles incoming requests and maps them to REST operations.
Performs request deserialization, response serialization, authentication and input validation.""" 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. # List of all permissions that must be checked.
permissions = ( permissions.FullAnonAccess, ) 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, # Allow name and description for the Resource to be set explicitly,
# overiding the default classname/docstring behaviour. # overiding the default classname/docstring behaviour.
# These are used for documentation in the standard html and text renderers. # 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)] return [method.upper() for method in self.http_method_names if hasattr(self, method)]
def http_method_not_allowed(self, request, *args, **kwargs): 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, raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method}) {'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, # Note: session based authentication is explicitly CSRF validated,
# all other authentication is CSRF exempt. # all other authentication is CSRF exempt.
@csrf_exempt @csrf_exempt
@ -92,7 +80,7 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
try: try:
# If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter # 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.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 # Authenticate and check request is has the relevant permissions
self._check_permissions() self._check_permissions()
@ -114,13 +102,14 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
response = Response(status.HTTP_204_NO_CONTENT) response = Response(status.HTTP_204_NO_CONTENT)
# Pre-serialize filtering (eg filter complex objects into natively serializable types) # 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: except ErrorResponse, exc:
response = exc.response response = exc.response
except: except:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
raise
# Always add these headers. # Always add these headers.
# #

View File

@ -1,10 +1,10 @@
"""The root view for the examples provided with Django REST framework""" """The root view for the examples provided with Django REST framework"""
from django.core.urlresolvers import reverse 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). """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. These examples are provided to help you get a better idea of the some of the features of RESTful APIs created using the framework.