aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2011-05-12 12:55:13 +0100
committerTom Christie2011-05-12 12:55:13 +0100
commit15f9e7c56699d31043782045a9fe47c354f612cb (patch)
tree2c58441416a877d0afba22d85aea691190a17fa1
parent4d126796752cc3c79a24fd9caed49da6c525096f (diff)
downloaddjango-rest-framework-15f9e7c56699d31043782045a9fe47c354f612cb.tar.bz2
refactoring resource specfic stuff into ResourceMixin - validators now defunct
-rw-r--r--djangorestframework/authentication.py4
-rw-r--r--djangorestframework/mixins.py126
-rw-r--r--djangorestframework/parsers.py19
-rw-r--r--djangorestframework/renderers.py14
-rw-r--r--djangorestframework/resource.py273
-rw-r--r--djangorestframework/tests/content.py30
-rw-r--r--djangorestframework/tests/files.py6
-rw-r--r--djangorestframework/tests/methods.py4
-rw-r--r--djangorestframework/tests/parsers.py18
-rw-r--r--djangorestframework/views.py25
-rw-r--r--examples/sandbox/views.py4
11 files changed, 372 insertions, 151 deletions
diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py
index 97e5d9c5..b0ba41aa 100644
--- a/djangorestframework/authentication.py
+++ b/djangorestframework/authentication.py
@@ -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
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index 65ebe171..d1c83c17 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -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):
diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py
index da700367..9e1b971b 100644
--- a/djangorestframework/parsers.py
+++ b/djangorestframework/parsers.py
@@ -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)
diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py
index bda2d38e..0aa30f70 100644
--- a/djangorestframework/renderers.py
+++ b/djangorestframework/renderers.py
@@ -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
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index 44178684..775d5288 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -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.
+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):
-# 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
+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(object):
+
+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))
diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py
index e566ea00..a99981fd 100644
--- a/djangorestframework/tests/content.py
+++ b/djangorestframework/tests/content.py
@@ -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)
diff --git a/djangorestframework/tests/files.py b/djangorestframework/tests/files.py
index f0321cb3..fc82fd83 100644
--- a/djangorestframework/tests/files.py
+++ b/djangorestframework/tests/files.py
@@ -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,
diff --git a/djangorestframework/tests/methods.py b/djangorestframework/tests/methods.py
index 0e74dc94..961d518b 100644
--- a/djangorestframework/tests/methods.py
+++ b/djangorestframework/tests/methods.py
@@ -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')
diff --git a/djangorestframework/tests/parsers.py b/djangorestframework/tests/parsers.py
index 88aad880..2720f4c7 100644
--- a/djangorestframework/tests/parsers.py
+++ b/djangorestframework/tests/parsers.py
@@ -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')
diff --git a/djangorestframework/views.py b/djangorestframework/views.py
index 3ce4e1d6..3abf101c 100644
--- a/djangorestframework/views.py
+++ b/djangorestframework/views.py
@@ -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.
#
diff --git a/examples/sandbox/views.py b/examples/sandbox/views.py
index 04e4da41..78b722ca 100644
--- a/examples/sandbox/views.py
+++ b/examples/sandbox/views.py
@@ -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.