Commit 15f9e7c5 by Tom Christie

refactoring resource specfic stuff into ResourceMixin - validators now defunct

parent 4d126796
......@@ -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
......
""""""
"""
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
......
......@@ -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.
#
......
"""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.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment