aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/topics/3.0-announcement.md344
-rw-r--r--rest_framework/authtoken/views.py2
-rw-r--r--rest_framework/fields.py51
-rw-r--r--rest_framework/filters.py6
-rw-r--r--rest_framework/generics.py4
-rw-r--r--rest_framework/mixins.py6
-rw-r--r--rest_framework/negotiation.py4
-rw-r--r--rest_framework/renderers.py4
-rw-r--r--rest_framework/request.py37
-rw-r--r--rest_framework/serializers.py21
-rw-r--r--tests/test_fields.py140
-rw-r--r--tests/test_generics.py2
-rw-r--r--tests/test_serializer.py62
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