diff options
| author | Tom Christie | 2011-05-12 12:55:13 +0100 | 
|---|---|---|
| committer | Tom Christie | 2011-05-12 12:55:13 +0100 | 
| commit | 15f9e7c56699d31043782045a9fe47c354f612cb (patch) | |
| tree | 2c58441416a877d0afba22d85aea691190a17fa1 | |
| parent | 4d126796752cc3c79a24fd9caed49da6c525096f (diff) | |
| download | django-rest-framework-15f9e7c56699d31043782045a9fe47c354f612cb.tar.bz2 | |
refactoring resource specfic stuff into ResourceMixin - validators now defunct
| -rw-r--r-- | djangorestframework/authentication.py | 4 | ||||
| -rw-r--r-- | djangorestframework/mixins.py | 126 | ||||
| -rw-r--r-- | djangorestframework/parsers.py | 19 | ||||
| -rw-r--r-- | djangorestframework/renderers.py | 14 | ||||
| -rw-r--r-- | djangorestframework/resource.py | 273 | ||||
| -rw-r--r-- | djangorestframework/tests/content.py | 30 | ||||
| -rw-r--r-- | djangorestframework/tests/files.py | 6 | ||||
| -rw-r--r-- | djangorestframework/tests/methods.py | 4 | ||||
| -rw-r--r-- | djangorestframework/tests/parsers.py | 18 | ||||
| -rw-r--r-- | djangorestframework/views.py | 25 | ||||
| -rw-r--r-- | examples/sandbox/views.py | 4 | 
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.  | 
