diff options
| -rw-r--r-- | docs/topics/3.0-announcement.md | 344 | ||||
| -rw-r--r-- | rest_framework/authtoken/views.py | 2 | ||||
| -rw-r--r-- | rest_framework/fields.py | 51 | ||||
| -rw-r--r-- | rest_framework/filters.py | 6 | ||||
| -rw-r--r-- | rest_framework/generics.py | 4 | ||||
| -rw-r--r-- | rest_framework/mixins.py | 6 | ||||
| -rw-r--r-- | rest_framework/negotiation.py | 4 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 4 | ||||
| -rw-r--r-- | rest_framework/request.py | 37 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 21 | ||||
| -rw-r--r-- | tests/test_fields.py | 140 | ||||
| -rw-r--r-- | tests/test_generics.py | 2 | ||||
| -rw-r--r-- | tests/test_serializer.py | 62 | 
13 files changed, 610 insertions, 73 deletions
| diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index cd883cdd..1795611c 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -4,36 +4,65 @@ See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-fr  # REST framework 3.0 -**Note incremental nature, discuss upgrading.** +**TODO**: Note incremental nature, discuss upgrading, motivation, features. -## Motivation - -**TODO** +* Serializer reprs. +* Non-magical model serializers. +* Base serializer class. +* Clean logic in views, serializers, fields.  ---  ## Request objects -#### The `request.data` property. +#### The `.data` and `.query_params` properties. -**TODO** +The usage of `request.DATA` and `request.FILES` is now discouraged in favor of a single `request.data` attribute that contains *all* the parsed data. -#### The parser API. +Having seperate attributes is reasonable for web applications that only ever parse URL encoded or MultiPart requests, but makes less sense for the general-purpose request parsing that REST framework supports. -**TODO** +You may now pass all the request data to a serializer class in a single argument: + +    ExampleSerializer(data=request.data) + +Instead of passing the files argument seperately: + +    # Don't do this... +    ExampleSerializer(data=request.DATA, files=request.FILES) + + +The usage of `request.QUERY_PARAMS` is now discouraged in favor of the lowercased `request.query_params`.  ## Serializers  #### Single-step object creation. +#### The `.create()` and `.update()` methods. +  **TODO**: Drop `.restore_object()`, use `.create()` and `.update()` which should save the instance. -**TODO**: Drop`.object`, use `.validated_data` or get the instance with `.save()`. +#### Use `.validated_data` instead of `.object`. -#### The `BaseSerializer` class. +You must now use the `.validated_data` attribute if you need to inspect the data before saving, rather than using the `.object` attribute, which no longer exists. -**TODO** +For example the following code *is no longer valid*: + +    if serializer.is_valid(): +        name = serializer.object.name  # Inspect validated field data. +        logging.info('Creating ticket "%s"' % name) +        serializer.object.user = request.user  # Include the user when saving. +        serializer.save() +Instead of using `.object` to inspect a partially constructed instance, you would now use `.validated_data` to inspect the cleaned incoming values. Also you can't set extra attributes on the instance directly, but instead pass them to the `.save()` method using the `extras` keyword argument. + +The corresponding code would now look like this: + +    if serializer.is_valid(): +        name = serializer.validated_data['name']  # Inspect validated field data. +        logging.info('Creating ticket "%s"' % name) +        extras = {'user': request.user}  # Include the user when saving. +        serializer.save(extras=extras) +   #### Always use `fields`, not `exclude`.  The `exclude` option is no longer available. You should use the more explicit `fields` option instead. @@ -111,42 +140,287 @@ These fields will be mapped to `serializers.ReadOnlyField()` instances.          message = CharField(max_length=1000)          expiry_date = ReadOnlyField() +#### The `ListSerializer` class. + +The `ListSerializer` class has now been added, and allows you to create base serializer classes for only accepting multiple inputs. + +    class MultipleUserSerializer(ListSerializer): +        child = UserSerializer() + +You can also still use the `many=True` argument to serializer classes. It's worth noting that `many=True` argument transparently creates a `ListSerializer` instance, allowing the validation logic for list and non-list data to be cleanly seperated in the REST framework codebase.  + +See also the new `ListField` class, which validates input in the same way, but does not include the serializer interfaces of `.is_valid()`, `.data`, `.save()` and so on. + +#### The `BaseSerializer` class. + +REST framework now includes a simple `BaseSerializer` class that can be used to easily support alternative serialization and deserialization styles. + +This class implements the same basic API as the `Serializer` class: + +* `.data` - Returns the outgoing primative representation. +* `.is_valid()` - Deserializes and validates incoming data. +* `.validated_data` - Returns the validated incoming data. +* `.errors` - Returns an errors during validation. +* `.save()` - Persists the validated data into an object instance. + +There are four mathods that can be overriding, depending on what functionality you want the serializer class to support: + +* `.to_representation()` - Override this to support serialization, for read operations. +* `.to_internal_value()` - Override this to support deserialization, for write operations. +* `.create()` and `.update()` - Overide either or both of these to support saving instances. + +##### Read-only serializers. + +To implement a read-only serializer using the `BaseSerializer` class, we just need to override the `.to_representation()` method. Let's take a look at an example using a simple Django model: + +    class HighScore(models.Model): +        created = models.DateTimeField(auto_now_add=True) +        player_name = models.CharField(max_length=10) +        score = models.IntegerField() + +It's simple to create a read-only serializer for converting `HighScore` instances into primative data types. + +    class HighScoreSerializer(serializers.BaseSerializer): +        def to_representation(self, obj): +            return { +                'score': obj.score, +                'player_name': obj.player_name +            } + +We can now use this class to serialize single `HighScore` instances: + +    @api_view(['GET']) +    def high_score(request, pk): +        instance = HighScore.objects.get(pk=pk) +        serializer = HighScoreSerializer(instance) +	    return Response(serializer.data) + +Or use it to serialize multiple instances: + +    @api_view(['GET']) +    def all_high_scores(request): +        queryset = HighScore.objects.order_by('-score') +        serializer = HighScoreSerializer(queryset, many=True) +	    return Response(serializer.data) + +##### Read-write serializers. + +To create a read-write serializer we first need to implement a `.to_internal_value()` method. This method returns the validated values that will be used to construct the object instance, and may raise a `ValidationError` if the supplied data is in an incorrect format. + +Once you've implemented `.to_internal_value()`, the basic validation API will be available on the serializer, and you will be able to use `.is_valid()`, `.validated_data` and `.errors`. + +If you want to also support `.save()` you'll need to also implement either or both of the `.create()` and `.update()` methods. + +Here's a complete example of our previous `HighScoreSerializer`, that's been updated to support both read and write operations. + +    class HighScoreSerializer(serializers.BaseSerializer): +        def to_internal_value(self, data): +            score = data.get('score') +            player_name = data.get('player_name') +             +            # Perform the data validation. +            if not score: +                raise ValidationError({ +                    'score': 'This field is required.' +                }) +            if not player_name: +                raise ValidationError({ +                    'player_name': 'This field is required.' +                }) +            if len(player_name) > 10: +                raise ValidationError({ +                    'player_name': 'May not be more than 10 characters.' +                }) +  +			# Return the validated values. This will be available as +			# the `.validated_data` property. +            return { +                'score': int(score), +                'player_name': player_name +            } + +        def to_representation(self, obj): +            return { +                'score': obj.score, +                'player_name': obj.player_name +            } +         +        def create(self, validated_data): +            return HighScore.objects.create(**validated_data) + +#### Creating new base classes with `BaseSerializer`. + +The `BaseSerializer` class is also useful if you want to implement new generic serializer classes for dealing with particular serialization styles or for integrating with different storage backends. + +The following class is an example of a generic serializer that can handle coercing aribitrary objects into primative representations.  + +    class ObjectSerializer(serializers.BaseSerializer): +        """ +        A read-only serializer that coerces arbitrary complex objects +        into primative representations. +        """ +        def to_representation(self, obj): +            for attribute_name in dir(obj): +                attribute = getattr(obj, attribute_name) +                if attribute_name('_'): +                    # Ignore private attributes. +                    pass +                elif hasattr(attribute, '__call__'): +                    # Ignore methods and other callables. +                    pass +                elif isinstance(attribute, (str, int, bool, float, type(None))): +                    # Primative types can be passed through unmodified. +                    output[attribute_name] = attribute +                elif isinstance(attribute, list): +                    # Recursivly deal with items in lists. +                    output[attribute_name] = [ +                        self.to_representation(item) for item in attribute +                    ] +                elif isinstance(attribute, dict): +                    # Recursivly deal with items in dictionarys. +                    output[attribute_name] = { +                        str(key): self.to_representation(value) +                        for key, value in attribute.items() +                    } +                else: +                    # Force anything else to its string representation. +                    output[attribute_name] = str(attribute)  ## Serializer fields  #### The `Field` and `ReadOnly` field classes. -**TODO** +There are some minor tweaks to the field base classes. + +Previously we had these two base classes: + +* `Field` as the base class for read-only fields. A default implementation was included for serializing data. +* `WriteableField` as the base class for read-write fields. + +We now use the following: + +* `Field` is the base class for all fields. It does not include any default implementation for either serializing or deserializing data. +* `ReadOnlyField` is a concrete implementation for read-only fields that simply returns the attribute value without modification. + +#### The `required`, `allow_none`, `allow_blank` and `default` arguments. + +REST framework now has more explict and clear control over validating empty values for fields. + +Previously the meaning of the `required=False` keyword argument was underspecified. In practice it's use meant that a field could either be not included in the input, or it could be included, but be `None`. + +We now have a better seperation, with seperate `required` and `allow_none` arguments. + +The following set of arguments are used to control validation of empty values: + +* `required=False`: The value does not need to be present in the input, and will not be passed to `.create()` or `.update()` if it is not seen. +* `default=<value>`: The value does not need to be present in the input, and a default value will be passed to `.create()` or `.update()` if it is not seen. +* `allow_none=True`: `None` is a valid input. +* `allow_blank=True`: `''` is valid input. For `CharField` and subclasses only. + +Typically you'll want to use `required=False` if the corresponding model field has a default value, and additionally set either `allow_none=True` or `allow_blank=True` if required. + +The `default` argument is there if you need it, but you'll more typically want defaults to be set on model fields, rather than serializer fields.  #### Coercing output types. -**TODO** +The previous field implementations did not forcibly coerce returned values into the correct type in many cases. For example, an `IntegerField` would return a string output if the attribute value was a string. We now more strictly coerce to the correct return type, leading to more constrained and expected behavior.  -#### The `ListSerializer` class. +#### The `ListField` class. -**TODO** +The `ListField` class has now been added. This field validates list input. It takes a `child` keyword argument which is used to specify the field used to validate each item in the list. For example: + +    scores = ListField(child=IntegerField(min_value=0, max_value=100)) + +You can also use a declarative style to create new subclasses of `ListField`, like this: + +    class ScoresField(ListField): +        child = IntegerField(min_value=0, max_value=100) + +We can now use the `ScoresField` class inside another serializer: + +    scores = ScoresField() + +See also the new `ListSerializer` class, which validates input in the same way, but also includes the serializer interfaces of `.is_valid()`, `.data`, `.save()` and so on. + +#### The `ChoiceField` class may now accept a flat list. + +The `ChoiceField` class may now accept a list of choices in addition to the existing style of using a list of pairs of `(name, display_value)`. The following is now valid: + +    color = ChoiceField(choices=['red', 'green', 'blue'])  #### The `MultipleChoiceField` class. -**TODO** +The `MultipleChoiceField` class has been added. This field acts like `ChoiceField`, but returns a set, which may include none, one or many of the valid choices.  #### Changes to the custom field API. -**TODO** `to_representation`, `to_internal_value`. +The `from_native(self, value)` and `to_native(self, data)` method names have been replaced with the more obviously named `to_representation(self, value)` and `to_internal_value(self, data)`. -#### Explicit `querysets` required on relational fields. +The `field_from_native()` and `field_to_native()` methods are removed. -**TODO** +#### Explicit `queryset` required on relational fields. + +Previously relational fields that were explicitly declared on a serializer class could omit the queryset argument if (and only if) they were declared on a `ModelSerializer`. + +This code *would be valid* in `2.4.3`: + +    class AccountSerializer(serializers.ModelSerializer): +        organisations = serializers.SlugRelatedField(slug_field='name') + +        class Meta: +            model = Account + +However this code *would not be valid* in `2.4.3`: + +    # Missing `queryset` +    class AccountSerializer(serializers.Serializer): +        organisations = serializers.SlugRelatedField(slug_field='name') + +        def restore_object(self, attrs, instance=None): +            # ... + +The queryset argument is now always required for writable relational fields. +This removes some magic and makes it easier and more obvious to move between implict `ModelSerializer` classes and explicit `Serializer` classes. + +    class AccountSerializer(serializers.ModelSerializer): +        organisations = serializers.SlugRelatedField( +            slug_field='name', +            queryset=Organisation.objects.all() +        ) + +        class Meta: +            model = Account + +The `queryset` argument is only ever required for writable fields, and is not required or valid for fields with `read_only=True`.  #### Optional argument to `SerializerMethodField`. -**TODO** +The argument to `SerializerMethodField` is now optional, and defaults to `get_<field_name>`. For example the following is valid: + +    class AccountSerializer(serializers.Serializer): +        # `method_name='get_billing_details'` by default. +        billing_details = serializers.SerializerMethodField() + +        def get_billing_details(self, account): +            return calculate_billing(account) + +In order to ensure a consistent code style an assertion error will be raised if you include a redundant method name argument that matches the default method name. For example, the following code *will raise an error*: + +    billing_details = serializers.SerializerMethodField('get_billing_details') + +#### Enforcing consistent `source` usage. + +I've see several codebases that unneccessarily include the `source` argument, setting it to the same value as the field name. This usage is redundant and confusing, making it less obvious that `source` is usually not required. + +The following usage will *now raise an error*: + +    email = serializers.EmailField(source='email')  ## Generic views  #### Simplification of view logic. -**TODO** +The view logic for the default method handlers has been significantly simplified, due to the new serializers API.  #### Removal of pre/post save hooks.  @@ -169,6 +443,20 @@ I would personally recommend that developers treat view instances as immutable o  #### PUT as create. +Allowing `PUT` as create operations is problematic, as it neccessarily exposes information about the existence or non-existance of objects. It's also not obvious that transparently allowing re-creating of previously deleted instances is neccessarily a better default behavior than simply returning `404` responses. + +Both styles "`PUT` as 404" and "`PUT` as create" can be valid in different circumstances, but we've now opted for the 404 behavior as the default, due to it being simpler and more obvious. + +If you need to restore the previous behavior you can include the `AllowPUTAsCreateMixin` class in your view. This class can be imported from `rest_framework.mixins`. + +#### Customizing error responses. + +The generic views now raise `ValidationError` for invalid data. This exception is then dealt with by the exception handler, rather than the view returning a `400 Bad Request` response directly. + +This change means that you can now easily cusomize the style of error responses across your entire API, without having to modify any of the generic views. + +## The metadata API +  **TODO**  ## API style @@ -241,3 +529,17 @@ Or modify it on an individual serializer field, using the `corece_to_string` key      )  The default JSON renderer will return float objects for uncoerced `Decimal` instances. This allows you to easily switch between string or float representations for decimals depending on your API design needs. + +## What's coming next. + +3.0 is an incremental release, and there are several upcoming features that will build on the baseline improvements that it makes. + +The 3.1 release is planned to address improvements in the following components: + +* Request parsing, mediatypes & the implementation of the browsable API. +* Introduction of a new pagination API. +* Better support for API versioning. + +The 3.2 release is planned to introduce an alternative admin-style interface to the browsable API. + +You can follow development on the GitHub site, where we use [milestones to indicate planning timescales](https://github.com/tomchristie/django-rest-framework/milestones). diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py index 94e6f061..103abb27 100644 --- a/rest_framework/authtoken/views.py +++ b/rest_framework/authtoken/views.py @@ -16,7 +16,7 @@ class ObtainAuthToken(APIView):      model = Token      def post(self, request): -        serializer = self.serializer_class(data=request.DATA) +        serializer = self.serializer_class(data=request.data)          if serializer.is_valid():              user = serializer.validated_data['user']              token, created = Token.objects.get_or_create(user=user) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 032bfd04..ec07a413 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -56,7 +56,7 @@ def get_attribute(instance, attrs):          except AttributeError as exc:              try:                  return instance[attr] -            except (KeyError, TypeError): +            except (KeyError, TypeError, AttributeError):                  raise exc      return instance @@ -90,6 +90,7 @@ NOT_READ_ONLY_WRITE_ONLY = 'May not set both `read_only` and `write_only`'  NOT_READ_ONLY_REQUIRED = 'May not set both `read_only` and `required`'  NOT_READ_ONLY_DEFAULT = 'May not set both `read_only` and `default`'  NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`' +USE_READONLYFIELD = 'Field(read_only=True) should be ReadOnlyField'  MISSING_ERROR_MESSAGE = (      'ValidationError raised by `{class_name}`, but error key `{key}` does '      'not exist in the `error_messages` dictionary.' @@ -105,9 +106,10 @@ class Field(object):      }      default_validators = []      default_empty_html = None +    initial = None      def __init__(self, read_only=False, write_only=False, -                 required=None, default=empty, initial=None, source=None, +                 required=None, default=empty, initial=empty, source=None,                   label=None, help_text=None, style=None,                   error_messages=None, validators=[], allow_null=False):          self._creation_counter = Field._creation_counter @@ -122,13 +124,14 @@ class Field(object):          assert not (read_only and required), NOT_READ_ONLY_REQUIRED          assert not (read_only and default is not empty), NOT_READ_ONLY_DEFAULT          assert not (required and default is not empty), NOT_REQUIRED_DEFAULT +        assert not (read_only and self.__class__ == Field), USE_READONLYFIELD          self.read_only = read_only          self.write_only = write_only          self.required = required          self.default = default          self.source = source -        self.initial = initial +        self.initial = self.initial if (initial is empty) else initial          self.label = label          self.help_text = help_text          self.style = {} if style is None else style @@ -146,24 +149,10 @@ class Field(object):          messages.update(error_messages or {})          self.error_messages = messages -    def __new__(cls, *args, **kwargs): -        """ -        When a field is instantiated, we store the arguments that were used, -        so that we can present a helpful representation of the object. -        """ -        instance = super(Field, cls).__new__(cls) -        instance._args = args -        instance._kwargs = kwargs -        return instance - -    def __deepcopy__(self, memo): -        args = copy.deepcopy(self._args) -        kwargs = copy.deepcopy(self._kwargs) -        return self.__class__(*args, **kwargs) -      def bind(self, field_name, parent):          """ -        Setup the context for the field instance. +        Initializes the field name and parent for the field instance. +        Called when a field is added to the parent serializer instance.          """          # In order to enforce a consistent style, we error if a redundant @@ -244,9 +233,9 @@ class Field(object):          validated data.          """          if data is empty: +            if getattr(self.root, 'partial', False): +                raise SkipField()              if self.required: -                if getattr(self.root, 'partial', False): -                    raise SkipField()                  self.fail('required')              return self.get_default() @@ -314,6 +303,25 @@ class Field(object):          """          return getattr(self.root, '_context', {}) +    def __new__(cls, *args, **kwargs): +        """ +        When a field is instantiated, we store the arguments that were used, +        so that we can present a helpful representation of the object. +        """ +        instance = super(Field, cls).__new__(cls) +        instance._args = args +        instance._kwargs = kwargs +        return instance + +    def __deepcopy__(self, memo): +        """ +        When cloning fields we instantiate using the arguments it was +        originally created with, rather than copying the complete state. +        """ +        args = copy.deepcopy(self._args) +        kwargs = copy.deepcopy(self._kwargs) +        return self.__class__(*args, **kwargs) +      def __repr__(self):          """          Fields are represented using their initial calling arguments. @@ -358,6 +366,7 @@ class NullBooleanField(Field):          'invalid': _('`{input}` is not a valid boolean.')      }      default_empty_html = None +    initial = None      TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))      FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))      NULL_VALUES = set(('n', 'N', 'null', 'Null', 'NULL', '', None)) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index c580f935..085dfe65 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -64,7 +64,7 @@ class DjangoFilterBackend(BaseFilterBackend):          filter_class = self.get_filter_class(view, queryset)          if filter_class: -            return filter_class(request.QUERY_PARAMS, queryset=queryset).qs +            return filter_class(request.query_params, queryset=queryset).qs          return queryset @@ -78,7 +78,7 @@ class SearchFilter(BaseFilterBackend):          Search terms are set by a ?search=... query parameter,          and may be comma and/or whitespace delimited.          """ -        params = request.QUERY_PARAMS.get(self.search_param, '') +        params = request.query_params.get(self.search_param, '')          return params.replace(',', ' ').split()      def construct_search(self, field_name): @@ -121,7 +121,7 @@ class OrderingFilter(BaseFilterBackend):          the `ordering_param` value on the OrderingFilter or by          specifying an `ORDERING_PARAM` value in the API settings.          """ -        params = request.QUERY_PARAMS.get(self.ordering_param) +        params = request.query_params.get(self.ordering_param)          if params:              return [param.strip() for param in params.split(',')] diff --git a/rest_framework/generics.py b/rest_framework/generics.py index f49b0a43..cf903dab 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -112,7 +112,7 @@ class GenericAPIView(views.APIView):          paginator = self.paginator_class(queryset, page_size)          page_kwarg = self.kwargs.get(self.page_kwarg) -        page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg) +        page_query_param = self.request.query_params.get(self.page_kwarg)          page = page_kwarg or page_query_param or 1          try:              page_number = paginator.validate_number(page) @@ -166,7 +166,7 @@ class GenericAPIView(views.APIView):          if self.paginate_by_param:              try:                  return strict_positive_int( -                    self.request.QUERY_PARAMS[self.paginate_by_param], +                    self.request.query_params[self.paginate_by_param],                      cutoff=self.max_paginate_by                  )              except (KeyError, ValueError): diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 14a6b44b..04b7a763 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -18,7 +18,7 @@ class CreateModelMixin(object):      Create a model instance.      """      def create(self, request, *args, **kwargs): -        serializer = self.get_serializer(data=request.DATA) +        serializer = self.get_serializer(data=request.data)          serializer.is_valid(raise_exception=True)          serializer.save()          headers = self.get_success_headers(serializer.data) @@ -62,7 +62,7 @@ class UpdateModelMixin(object):      def update(self, request, *args, **kwargs):          partial = kwargs.pop('partial', False)          instance = self.get_object() -        serializer = self.get_serializer(instance, data=request.DATA, partial=partial) +        serializer = self.get_serializer(instance, data=request.data, partial=partial)          serializer.is_valid(raise_exception=True)          serializer.save()          return Response(serializer.data) @@ -95,7 +95,7 @@ class AllowPUTAsCreateMixin(object):      def update(self, request, *args, **kwargs):          partial = kwargs.pop('partial', False)          instance = self.get_object_or_none() -        serializer = self.get_serializer(instance, data=request.DATA, partial=partial) +        serializer = self.get_serializer(instance, data=request.data, partial=partial)          serializer.is_valid(raise_exception=True)          if instance is None: diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index ca7b5397..1838130a 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -38,7 +38,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):          """          # Allow URL style format override.  eg. "?format=json          format_query_param = self.settings.URL_FORMAT_OVERRIDE -        format = format_suffix or request.QUERY_PARAMS.get(format_query_param) +        format = format_suffix or request.query_params.get(format_query_param)          if format:              renderers = self.filter_renderers(renderers, format) @@ -87,5 +87,5 @@ class DefaultContentNegotiation(BaseContentNegotiation):          Allows URL style accept override.  eg. "?accept=application/json"          """          header = request.META.get('HTTP_ACCEPT', '*/*') -        header = request.QUERY_PARAMS.get(self.settings.URL_ACCEPT_OVERRIDE, header) +        header = request.query_params.get(self.settings.URL_ACCEPT_OVERRIDE, header)          return [token.strip() for token in header.split(',')] diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 3bf03e62..225f9fe8 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -120,7 +120,7 @@ class JSONPRenderer(JSONRenderer):          Determine the name of the callback to wrap around the json output.          """          request = renderer_context.get('request', None) -        params = request and request.QUERY_PARAMS or {} +        params = request and request.query_params or {}          return params.get(self.callback_parameter, self.default_callback)      def render(self, data, accepted_media_type=None, renderer_context=None): @@ -426,7 +426,7 @@ class BrowsableAPIRenderer(BaseRenderer):          """          if request.method == method:              try: -                data = request.DATA +                data = request.data                  # files = request.FILES              except ParseError:                  data = None diff --git a/rest_framework/request.py b/rest_framework/request.py index 27532661..d80baa70 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -4,7 +4,7 @@ The Request class is used as a wrapper around the standard request object.  The wrapped request then offers a richer API, in particular :      - content automatically parsed according to `Content-Type` header, -      and available as `request.DATA` +      and available as `request.data`      - full support of PUT method, including support for file uploads      - form overloading of HTTP method, content type and content  """ @@ -13,6 +13,7 @@ from django.conf import settings  from django.http import QueryDict  from django.http.multipartparser import parse_header  from django.utils.datastructures import MultiValueDict +from django.utils.datastructures import MergeDict as DjangoMergeDict  from rest_framework import HTTP_HEADER_ENCODING  from rest_framework import exceptions  from rest_framework.compat import BytesIO @@ -58,6 +59,15 @@ class override_method(object):              self.view.action = self.action +class MergeDict(DjangoMergeDict, dict): +    """ +    Using this as a workaround until the parsers API is properly +    addressed in 3.1. +    """ +    def __init__(self, *dicts): +        self.dicts = dicts + +  class Empty(object):      """      Placeholder for unset attributes. @@ -82,6 +92,7 @@ def clone_request(request, method):                    parser_context=request.parser_context)      ret._data = request._data      ret._files = request._files +    ret._full_data = request._full_data      ret._content_type = request._content_type      ret._stream = request._stream      ret._method = method @@ -133,6 +144,7 @@ class Request(object):          self.parser_context = parser_context          self._data = Empty          self._files = Empty +        self._full_data = Empty          self._method = Empty          self._content_type = Empty          self._stream = Empty @@ -186,13 +198,26 @@ class Request(object):          return self._stream      @property -    def QUERY_PARAMS(self): +    def query_params(self):          """          More semantically correct name for request.GET.          """          return self._request.GET      @property +    def QUERY_PARAMS(self): +        """ +        Synonym for `.query_params`, for backwards compatibility. +        """ +        return self._request.GET + +    @property +    def data(self): +        if not _hasattr(self, '_full_data'): +            self._load_data_and_files() +        return self._full_data + +    @property      def DATA(self):          """          Parses the request body and returns the data. @@ -272,6 +297,10 @@ class Request(object):          if not _hasattr(self, '_data'):              self._data, self._files = self._parse() +            if self._files: +                self._full_data = MergeDict(self._data, self._files) +            else: +                self._full_data = self._data      def _load_method_and_content_type(self):          """ @@ -333,6 +362,7 @@ class Request(object):          # At this point we're committed to parsing the request as form data.          self._data = self._request.POST          self._files = self._request.FILES +        self._full_data = MergeDict(self._data, self._files)          # Method overloading - change the method and remove the param from the content.          if ( @@ -350,7 +380,7 @@ class Request(object):          ):              self._content_type = self._data[self._CONTENTTYPE_PARAM]              self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding'])) -            self._data, self._files = (Empty, Empty) +            self._data, self._files, self._full_data = (Empty, Empty, Empty)      def _parse(self):          """ @@ -380,6 +410,7 @@ class Request(object):              # logging the request or similar.              self._data = QueryDict('', encoding=self._request._encoding)              self._files = MultiValueDict() +            self._full_data = self._data              raise          # Parser classes may return the raw data, or a diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b6a1898c..a2b878ec 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -57,21 +57,24 @@ class BaseSerializer(Field):      def to_representation(self, instance):          raise NotImplementedError('`to_representation()` must be implemented.') -    def update(self, instance, attrs): +    def update(self, instance, validated_data):          raise NotImplementedError('`update()` must be implemented.') -    def create(self, attrs): +    def create(self, validated_data):          raise NotImplementedError('`create()` must be implemented.')      def save(self, extras=None): -        attrs = self.validated_data +        validated_data = self.validated_data          if extras is not None: -            attrs = dict(list(attrs.items()) + list(extras.items())) +            validated_data = dict( +                list(validated_data.items()) + +                list(extras.items()) +            )          if self.instance is not None: -            self.update(self.instance, attrs) +            self.update(self.instance, validated_data)          else: -            self.instance = self.create(attrs) +            self.instance = self.create(validated_data)          return self.instance @@ -321,12 +324,6 @@ class ListSerializer(BaseSerializer):      def create(self, attrs_list):          return [self.child.create(attrs) for attrs in attrs_list] -    def save(self): -        if self.instance is not None: -            self.update(self.instance, self.validated_data) -        self.instance = self.create(self.validated_data) -        return self.instance -      def __repr__(self):          return representation.list_repr(self, indent=1) diff --git a/tests/test_fields.py b/tests/test_fields.py index b29acad8..1539a210 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -9,7 +9,10 @@ import pytest  # Tests for field keyword arguments and core functionality.  # --------------------------------------------------------- -class TestFieldOptions: +class TestEmpty: +    """ +    Tests for `required`, `allow_null`, `allow_blank`, `default`. +    """      def test_required(self):          """          By default a field must be included in the input. @@ -69,6 +72,17 @@ class TestFieldOptions:          output = field.run_validation()          assert output is 123 + +class TestSource: +    def test_source(self): +        class ExampleSerializer(serializers.Serializer): +            example_field = serializers.CharField(source='other') +        serializer = ExampleSerializer(data={'example_field': 'abc'}) +        print serializer.is_valid() +        print serializer.data +        assert serializer.is_valid() +        assert serializer.validated_data == {'other': 'abc'} +      def test_redundant_source(self):          class ExampleSerializer(serializers.Serializer):              example_field = serializers.CharField(source='example_field') @@ -81,6 +95,128 @@ class TestFieldOptions:          ) +class TestReadOnly: +    def setup(self): +        class TestSerializer(serializers.Serializer): +            read_only = fields.ReadOnlyField() +            writable = fields.IntegerField() +        self.Serializer = TestSerializer + +    def test_validate_read_only(self): +        """ +        Read-only fields should not be included in validation. +        """ +        data = {'read_only': 123, 'writable': 456} +        serializer = self.Serializer(data=data) +        assert serializer.is_valid() +        assert serializer.validated_data == {'writable': 456} + +    def test_serialize_read_only(self): +        """ +        Read-only fields should be serialized. +        """ +        instance = {'read_only': 123, 'writable': 456} +        serializer = self.Serializer(instance) +        assert serializer.data == {'read_only': 123, 'writable': 456} + + +class TestWriteOnly: +    def setup(self): +        class TestSerializer(serializers.Serializer): +            write_only = fields.IntegerField(write_only=True) +            readable = fields.IntegerField() +        self.Serializer = TestSerializer + +    def test_validate_write_only(self): +        """ +        Write-only fields should be included in validation. +        """ +        data = {'write_only': 123, 'readable': 456} +        serializer = self.Serializer(data=data) +        assert serializer.is_valid() +        assert serializer.validated_data == {'write_only': 123, 'readable': 456} + +    def test_serialize_write_only(self): +        """ +        Write-only fields should not be serialized. +        """ +        instance = {'write_only': 123, 'readable': 456} +        serializer = self.Serializer(instance) +        assert serializer.data == {'readable': 456} + + +class TestInitial: +    def setup(self): +        class TestSerializer(serializers.Serializer): +            initial_field = fields.IntegerField(initial=123) +            blank_field = fields.IntegerField() +        self.serializer = TestSerializer() + +    def test_initial(self): +        """ +        Initial values should be included when serializing a new representation. +        """ +        assert self.serializer.data == { +            'initial_field': 123, +            'blank_field': None +        } + + +class TestLabel: +    def setup(self): +        class TestSerializer(serializers.Serializer): +            labeled = fields.IntegerField(label='My label') +        self.serializer = TestSerializer() + +    def test_label(self): +        """ +        A field's label may be set with the `label` argument. +        """ +        fields = self.serializer.fields +        assert fields['labeled'].label == 'My label' + + +class TestInvalidErrorKey: +    def setup(self): +        class ExampleField(serializers.Field): +            def to_native(self, data): +                self.fail('incorrect') +        self.field = ExampleField() + +    def test_invalid_error_key(self): +        """ +        If a field raises a validation error, but does not have a corresponding +        error message, then raise an appropriate assertion error. +        """ +        with pytest.raises(AssertionError) as exc_info: +            self.field.to_native(123) +        expected = ( +            'ValidationError raised by `ExampleField`, but error key ' +            '`incorrect` does not exist in the `error_messages` dictionary.' +        ) +        assert str(exc_info.value) == expected + + +class TestBooleanHTMLInput: +    def setup(self): +        class TestSerializer(serializers.Serializer): +            archived = fields.BooleanField() +        self.Serializer = TestSerializer + +    def test_empty_html_checkbox(self): +        """ +        HTML checkboxes do not send any value, but should be treated +        as `False` by BooleanField. +        """ +        # This class mocks up a dictionary like object, that behaves +        # as if it was returned for multipart or urlencoded data. +        class MockHTMLDict(dict): +            getlist = None +        serializer = self.Serializer(data=MockHTMLDict()) +        assert serializer.is_valid() +        assert serializer.validated_data == {'archived': False} + +  # Tests for field input and output values.  # ---------------------------------------- @@ -495,7 +631,7 @@ class TestDateTimeField(FieldValues):          '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),          datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),          datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()), -        # Note that 1.4 does not support timezone string parsing. +        # Django 1.4 does not support timezone string parsing.          '2001-01-01T14:00+01:00' if (django.VERSION > (1, 4)) else '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC())      }      invalid_inputs = { diff --git a/tests/test_generics.py b/tests/test_generics.py index 89f9def0..2690fb47 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -38,7 +38,7 @@ class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):  class SlugSerializer(serializers.ModelSerializer): -    slug = serializers.Field(read_only=True) +    slug = serializers.ReadOnlyField()      class Meta:          model = SlugBasedModel diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 5646f994..256a12e6 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -1,4 +1,5 @@  from rest_framework import serializers +import pytest  # Tests for core functionality. @@ -29,6 +30,67 @@ class TestSerializer:          assert serializer.validated_data == {'char': 'abc'}          assert serializer.errors == {} +    def test_empty_serializer(self): +        serializer = self.Serializer() +        assert serializer.data == {'char': '', 'integer': None} + +    def test_missing_attribute_during_serialization(self): +        class MissingAttributes: +            pass +        instance = MissingAttributes() +        serializer = self.Serializer(instance) +        with pytest.raises(AttributeError): +            serializer.data + + +class TestStarredSource: +    """ +    Tests for `source='*'` argument, which is used for nested representations. + +    For example: + +        nested_field = NestedField(source='*') +    """ +    data = { +        'nested1': {'a': 1, 'b': 2}, +        'nested2': {'c': 3, 'd': 4} +    } + +    def setup(self): +        class NestedSerializer1(serializers.Serializer): +            a = serializers.IntegerField() +            b = serializers.IntegerField() + +        class NestedSerializer2(serializers.Serializer): +            c = serializers.IntegerField() +            d = serializers.IntegerField() + +        class TestSerializer(serializers.Serializer): +            nested1 = NestedSerializer1(source='*') +            nested2 = NestedSerializer2(source='*') + +        self.Serializer = TestSerializer + +    def test_nested_validate(self): +        """ +        A nested representation is validated into a flat internal object. +        """ +        serializer = self.Serializer(data=self.data) +        assert serializer.is_valid() +        assert serializer.validated_data == { +            'a': 1, +            'b': 2, +            'c': 3, +            'd': 4 +        } + +    def test_nested_serialize(self): +        """ +        An object can be serialized into a nested representation. +        """ +        instance = {'a': 1, 'b': 2, 'c': 3, 'd': 4} +        serializer = self.Serializer(instance) +        assert serializer.data == self.data  # # -*- coding: utf-8 -*-  # from __future__ import unicode_literals | 
