From 15f9e7c56699d31043782045a9fe47c354f612cb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 12 May 2011 12:55:13 +0100 Subject: [PATCH] refactoring resource specfic stuff into ResourceMixin - validators now defunct --- djangorestframework/authentication.py | 4 +- djangorestframework/mixins.py | 126 ++++++------ djangorestframework/parsers.py | 19 +- djangorestframework/renderers.py | 14 +- djangorestframework/resource.py | 275 +++++++++++++++++++++++--- djangorestframework/tests/content.py | 30 +-- djangorestframework/tests/files.py | 6 +- djangorestframework/tests/methods.py | 4 +- djangorestframework/tests/parsers.py | 18 +- djangorestframework/views.py | 25 +-- examples/sandbox/views.py | 4 +- 11 files changed, 373 insertions(+), 152 deletions(-) diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py index 97e5d9c5a..b0ba41aae 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 65ebe1715..d1c83c177 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 da700367d..9e1b971bb 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 bda2d38e9..0aa30f707 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 441786842..775d52889 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. - -#class Resource(object): -# def __init__(self, view): -# self.view = view -# -# def object_to_data(self, obj): -# pass -# -# def data_to_object(self, data, files): -# pass -# -#class FormResource(object): -# pass -# -#class ModelResource(object): -# pass +def _form_to_data(form): + """ + Returns a dict containing the data in a form instance. + + This code is pretty much a clone of the ``Form.as_p()`` ``Form.as_ul`` + and ``Form.as_table()`` methods, except that it returns data suitable + for arbitrary serialization, rather than rendering the result directly + into html. + """ + ret = {} + for name, field in form.fields.items(): + if not form.is_bound: + data = form.initial.get(name, field.initial) + if callable(data): + data = data() + else: + if isinstance(field, FileField) and form.data is None: + data = form.initial.get(name, field.initial) + else: + data = field.widget.value_from_datadict(form.data, form.files, name) + ret[name] = field.prepare_value(data) + return ret -class Resource(object): +class BaseResource(object): + """Base class for all Resource classes, which simply defines the interface they provide.""" + + def __init__(self, view): + self.view = view + + def validate(self, data, files): + """Given some content as input return some cleaned, validated content. + Typically raises a ErrorResponse with status code 400 (Bad Request) on failure. + + Must be overridden to be implemented.""" + return data + + def object_to_data(self, obj): + return _object_to_data(obj) + + +class Resource(BaseResource): """ A Resource determines how a python object maps to some serializable data. Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets. @@ -99,9 +121,11 @@ class Resource(object): # you should explicitly set the fields attribute on your class. fields = None - @classmethod - def object_to_serializable(self, data): - """A (horrible) munging of Piston's pre-serialization. Returns a dict""" + # TODO: Replace this with new Serializer code based on Forms API. + def object_to_data(self, obj): + """ + A (horrible) munging of Piston's pre-serialization. Returns a dict. + """ def _any(thing, fields=()): """ @@ -321,5 +345,208 @@ class Resource(object): return dict([ (k, _any(v)) for k, v in data.iteritems() ]) # Kickstart the seralizin'. - return _any(data, self.fields) + return _any(obj, self.fields) + +class FormResource(Resource): + """Validator class that uses forms for validation. + Also provides a get_bound_form() method which may be used by some renderers. + + The view class should provide `.form` attribute which specifies the form classmethod + to be used for validation. + + On calling validate() this validator may set a `.bound_form_instance` attribute on the + view, which may be used by some renderers.""" + + + def validate(self, data, files): + """ + Given some content as input return some cleaned, validated content. + Raises a ErrorResponse with status code 400 (Bad Request) on failure. + + Validation is standard form validation, with an additional constraint that no extra unknown fields may be supplied. + + On failure the ErrorResponse content is a dict which may contain 'errors' and 'field-errors' keys. + If the 'errors' key exists it is a list of strings of non-field errors. + If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}. + """ + return self._validate(data, files) + + + def _validate(self, data, files, allowed_extra_fields=()): + """ + Wrapped by validate to hide the extra_fields option that the ModelValidatorMixin uses. + extra_fields is a list of fields which are not defined by the form, but which we still + expect to see on the input. + """ + bound_form = self.get_bound_form(data, files) + + if bound_form is None: + return data + + self.view.bound_form_instance = bound_form + + seen_fields_set = set(data.keys()) + form_fields_set = set(bound_form.fields.keys()) + allowed_extra_fields_set = set(allowed_extra_fields) + + # In addition to regular validation we also ensure no additional fields are being passed in... + unknown_fields = seen_fields_set - (form_fields_set | allowed_extra_fields_set) + + # Check using both regular validation, and our stricter no additional fields rule + if bound_form.is_valid() and not unknown_fields: + # Validation succeeded... + cleaned_data = bound_form.cleaned_data + + cleaned_data.update(bound_form.files) + + # Add in any extra fields to the cleaned content... + for key in (allowed_extra_fields_set & seen_fields_set) - set(cleaned_data.keys()): + cleaned_data[key] = data[key] + + return cleaned_data + + # Validation failed... + detail = {} + + if not bound_form.errors and not unknown_fields: + detail = {u'errors': [u'No content was supplied.']} + + else: + # Add any non-field errors + if bound_form.non_field_errors(): + detail[u'errors'] = bound_form.non_field_errors() + + # Add standard field errors + field_errors = dict((key, map(unicode, val)) + for (key, val) + in bound_form.errors.iteritems() + if not key.startswith('__')) + + # Add any unknown field errors + for key in unknown_fields: + field_errors[key] = [u'This field does not exist.'] + + if field_errors: + detail[u'field-errors'] = field_errors + + # Return HTTP 400 response (BAD REQUEST) + raise ErrorResponse(400, detail) + + + def get_bound_form(self, data=None, files=None): + """Given some content return a Django form bound to that content. + If form validation is turned off (form class attribute is None) then returns None.""" + form_cls = getattr(self, 'form', None) + + if not form_cls: + return None + + if data is not None: + return form_cls(data, files) + + return form_cls() + + +class ModelResource(FormResource): + """Validator class that uses forms for validation and otherwise falls back to a model form if no form is set. + Also provides a get_bound_form() method which may be used by some renderers.""" + + """The form class that should be used for validation, or None to use model form validation.""" + form = None + + """The model class from which the model form should be constructed if no form is set.""" + model = None + + """The list of fields we expect to receive as input. Fields in this list will may be received with + raising non-existent field errors, even if they do not exist as fields on the ModelForm. + + Setting the fields class attribute causes the exclude_fields class attribute to be disregarded.""" + fields = None + + """The list of fields to exclude from the Model. This is only used if the fields class attribute is not set.""" + exclude_fields = ('id', 'pk') + + + # TODO: test the different validation here to allow for get get_absolute_url to be supplied on input and not bork out + # TODO: be really strict on fields - check they match in the handler methods. (this isn't a validator thing tho.) + def validate(self, data, files): + """ + Given some content as input return some cleaned, validated content. + Raises a ErrorResponse with status code 400 (Bad Request) on failure. + + Validation is standard form or model form validation, + with an additional constraint that no extra unknown fields may be supplied, + and that all fields specified by the fields class attribute must be supplied, + even if they are not validated by the form/model form. + + On failure the ErrorResponse content is a dict which may contain 'errors' and 'field-errors' keys. + If the 'errors' key exists it is a list of strings of non-field errors. + If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}. + """ + return self._validate(data, files, allowed_extra_fields=self._property_fields_set) + + + def get_bound_form(self, data=None, files=None): + """Given some content return a Django form bound to that content. + + If the form class attribute has been explicitly set then use that class to create a Form, + otherwise if model is set use that class to create a ModelForm, otherwise return None.""" + + form_cls = getattr(self, 'form', None) + model_cls = getattr(self, 'model', None) + + if form_cls: + # Use explict Form + return super(ModelFormValidator, self).get_bound_form(data, files) + + elif model_cls: + # Fall back to ModelForm which we create on the fly + class OnTheFlyModelForm(forms.ModelForm): + class Meta: + model = model_cls + #fields = tuple(self._model_fields_set) + + # Instantiate the ModelForm as appropriate + if content and isinstance(content, models.Model): + # Bound to an existing model instance + return OnTheFlyModelForm(instance=content) + elif not data is None: + return OnTheFlyModelForm(data, files) + return OnTheFlyModelForm() + + # Both form and model not set? Okay bruv, whatevs... + return None + + + @property + def _model_fields_set(self): + """Return a set containing the names of validated fields on the model.""" + resource = self.view.resource + model = getattr(resource, 'model', None) + fields = getattr(resource, 'fields', self.fields) + exclude_fields = getattr(resource, 'exclude_fields', self.exclude_fields) + + model_fields = set(field.name for field in model._meta.fields) + + if fields: + return model_fields & set(as_tuple(fields)) + + return model_fields - set(as_tuple(exclude_fields)) + + @property + def _property_fields_set(self): + """Returns a set containing the names of validated properties on the model.""" + resource = self.view.resource + model = getattr(resource, 'model', None) + fields = getattr(resource, 'fields', self.fields) + exclude_fields = getattr(resource, 'exclude_fields', self.exclude_fields) + + property_fields = set(attr for attr in dir(model) if + isinstance(getattr(model, attr, None), property) + and not attr.startswith('_')) + + if fields: + return property_fields & set(as_tuple(fields)) + + return property_fields - set(as_tuple(exclude_fields)) diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py index e566ea009..a99981fd1 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 f0321cb32..fc82fd83f 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 0e74dc948..961d518b6 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 88aad8805..2720f4c77 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 3ce4e1d69..3abf101c0 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 04e4da412..78b722caf 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.