request.data attribute

This commit is contained in:
Tom Christie 2014-09-26 10:46:52 +01:00
parent 417fe1b675
commit 2859eaf524
13 changed files with 610 additions and 73 deletions

View File

@ -4,35 +4,64 @@ See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-fr
# REST framework 3.0 # REST framework 3.0
**Note incremental nature, discuss upgrading.** **TODO**: Note incremental nature, discuss upgrading, motivation, features.
## Motivation * Serializer reprs.
* Non-magical model serializers.
**TODO** * Base serializer class.
* Clean logic in views, serializers, fields.
--- ---
## Request objects ## 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 ## Serializers
#### Single-step object creation. #### Single-step object creation.
#### The `.create()` and `.update()` methods.
**TODO**: Drop `.restore_object()`, use `.create()` and `.update()` which should save the instance. **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`. #### Always use `fields`, not `exclude`.
@ -111,42 +140,287 @@ These fields will be mapped to `serializers.ReadOnlyField()` instances.
message = CharField(max_length=1000) message = CharField(max_length=1000)
expiry_date = ReadOnlyField() 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 ## Serializer fields
#### The `Field` and `ReadOnly` field classes. #### 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. #### 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. #### 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. #### 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`. #### 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 ## Generic views
#### Simplification of view logic. #### 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. #### 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. #### 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** **TODO**
## API style ## 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. 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).

View File

@ -16,7 +16,7 @@ class ObtainAuthToken(APIView):
model = Token model = Token
def post(self, request): def post(self, request):
serializer = self.serializer_class(data=request.DATA) serializer = self.serializer_class(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
user = serializer.validated_data['user'] user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user) token, created = Token.objects.get_or_create(user=user)

View File

@ -56,7 +56,7 @@ def get_attribute(instance, attrs):
except AttributeError as exc: except AttributeError as exc:
try: try:
return instance[attr] return instance[attr]
except (KeyError, TypeError): except (KeyError, TypeError, AttributeError):
raise exc raise exc
return instance 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_REQUIRED = 'May not set both `read_only` and `required`'
NOT_READ_ONLY_DEFAULT = 'May not set both `read_only` and `default`' NOT_READ_ONLY_DEFAULT = 'May not set both `read_only` and `default`'
NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`' NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`'
USE_READONLYFIELD = 'Field(read_only=True) should be ReadOnlyField'
MISSING_ERROR_MESSAGE = ( MISSING_ERROR_MESSAGE = (
'ValidationError raised by `{class_name}`, but error key `{key}` does ' 'ValidationError raised by `{class_name}`, but error key `{key}` does '
'not exist in the `error_messages` dictionary.' 'not exist in the `error_messages` dictionary.'
@ -105,9 +106,10 @@ class Field(object):
} }
default_validators = [] default_validators = []
default_empty_html = None default_empty_html = None
initial = None
def __init__(self, read_only=False, write_only=False, 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, label=None, help_text=None, style=None,
error_messages=None, validators=[], allow_null=False): error_messages=None, validators=[], allow_null=False):
self._creation_counter = Field._creation_counter 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 required), NOT_READ_ONLY_REQUIRED
assert not (read_only and default is not empty), NOT_READ_ONLY_DEFAULT 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 (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.read_only = read_only
self.write_only = write_only self.write_only = write_only
self.required = required self.required = required
self.default = default self.default = default
self.source = source self.source = source
self.initial = initial self.initial = self.initial if (initial is empty) else initial
self.label = label self.label = label
self.help_text = help_text self.help_text = help_text
self.style = {} if style is None else style self.style = {} if style is None else style
@ -146,24 +149,10 @@ class Field(object):
messages.update(error_messages or {}) messages.update(error_messages or {})
self.error_messages = messages 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): 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 # In order to enforce a consistent style, we error if a redundant
@ -244,9 +233,9 @@ class Field(object):
validated data. validated data.
""" """
if data is empty: if data is empty:
if getattr(self.root, 'partial', False):
raise SkipField()
if self.required: if self.required:
if getattr(self.root, 'partial', False):
raise SkipField()
self.fail('required') self.fail('required')
return self.get_default() return self.get_default()
@ -314,6 +303,25 @@ class Field(object):
""" """
return getattr(self.root, '_context', {}) 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): def __repr__(self):
""" """
Fields are represented using their initial calling arguments. Fields are represented using their initial calling arguments.
@ -358,6 +366,7 @@ class NullBooleanField(Field):
'invalid': _('`{input}` is not a valid boolean.') 'invalid': _('`{input}` is not a valid boolean.')
} }
default_empty_html = None default_empty_html = None
initial = None
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False)) FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))
NULL_VALUES = set(('n', 'N', 'null', 'Null', 'NULL', '', None)) NULL_VALUES = set(('n', 'N', 'null', 'Null', 'NULL', '', None))

View File

@ -64,7 +64,7 @@ class DjangoFilterBackend(BaseFilterBackend):
filter_class = self.get_filter_class(view, queryset) filter_class = self.get_filter_class(view, queryset)
if filter_class: if filter_class:
return filter_class(request.QUERY_PARAMS, queryset=queryset).qs return filter_class(request.query_params, queryset=queryset).qs
return queryset return queryset
@ -78,7 +78,7 @@ class SearchFilter(BaseFilterBackend):
Search terms are set by a ?search=... query parameter, Search terms are set by a ?search=... query parameter,
and may be comma and/or whitespace delimited. 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() return params.replace(',', ' ').split()
def construct_search(self, field_name): def construct_search(self, field_name):
@ -121,7 +121,7 @@ class OrderingFilter(BaseFilterBackend):
the `ordering_param` value on the OrderingFilter or by the `ordering_param` value on the OrderingFilter or by
specifying an `ORDERING_PARAM` value in the API settings. 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: if params:
return [param.strip() for param in params.split(',')] return [param.strip() for param in params.split(',')]

View File

@ -112,7 +112,7 @@ class GenericAPIView(views.APIView):
paginator = self.paginator_class(queryset, page_size) paginator = self.paginator_class(queryset, page_size)
page_kwarg = self.kwargs.get(self.page_kwarg) 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 page = page_kwarg or page_query_param or 1
try: try:
page_number = paginator.validate_number(page) page_number = paginator.validate_number(page)
@ -166,7 +166,7 @@ class GenericAPIView(views.APIView):
if self.paginate_by_param: if self.paginate_by_param:
try: try:
return strict_positive_int( 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 cutoff=self.max_paginate_by
) )
except (KeyError, ValueError): except (KeyError, ValueError):

View File

@ -18,7 +18,7 @@ class CreateModelMixin(object):
Create a model instance. Create a model instance.
""" """
def create(self, request, *args, **kwargs): 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.is_valid(raise_exception=True)
serializer.save() serializer.save()
headers = self.get_success_headers(serializer.data) headers = self.get_success_headers(serializer.data)
@ -62,7 +62,7 @@ class UpdateModelMixin(object):
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False) partial = kwargs.pop('partial', False)
instance = self.get_object() 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.is_valid(raise_exception=True)
serializer.save() serializer.save()
return Response(serializer.data) return Response(serializer.data)
@ -95,7 +95,7 @@ class AllowPUTAsCreateMixin(object):
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False) partial = kwargs.pop('partial', False)
instance = self.get_object_or_none() 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) serializer.is_valid(raise_exception=True)
if instance is None: if instance is None:

View File

@ -38,7 +38,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
""" """
# Allow URL style format override. eg. "?format=json # Allow URL style format override. eg. "?format=json
format_query_param = self.settings.URL_FORMAT_OVERRIDE 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: if format:
renderers = self.filter_renderers(renderers, format) renderers = self.filter_renderers(renderers, format)
@ -87,5 +87,5 @@ class DefaultContentNegotiation(BaseContentNegotiation):
Allows URL style accept override. eg. "?accept=application/json" Allows URL style accept override. eg. "?accept=application/json"
""" """
header = request.META.get('HTTP_ACCEPT', '*/*') 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(',')] return [token.strip() for token in header.split(',')]

View File

@ -120,7 +120,7 @@ class JSONPRenderer(JSONRenderer):
Determine the name of the callback to wrap around the json output. Determine the name of the callback to wrap around the json output.
""" """
request = renderer_context.get('request', None) 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) return params.get(self.callback_parameter, self.default_callback)
def render(self, data, accepted_media_type=None, renderer_context=None): def render(self, data, accepted_media_type=None, renderer_context=None):
@ -426,7 +426,7 @@ class BrowsableAPIRenderer(BaseRenderer):
""" """
if request.method == method: if request.method == method:
try: try:
data = request.DATA data = request.data
# files = request.FILES # files = request.FILES
except ParseError: except ParseError:
data = None data = None

View File

@ -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 : The wrapped request then offers a richer API, in particular :
- content automatically parsed according to `Content-Type` header, - 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 - full support of PUT method, including support for file uploads
- form overloading of HTTP method, content type and content - 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 import QueryDict
from django.http.multipartparser import parse_header from django.http.multipartparser import parse_header
from django.utils.datastructures import MultiValueDict 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 HTTP_HEADER_ENCODING
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework.compat import BytesIO from rest_framework.compat import BytesIO
@ -58,6 +59,15 @@ class override_method(object):
self.view.action = self.action 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): class Empty(object):
""" """
Placeholder for unset attributes. Placeholder for unset attributes.
@ -82,6 +92,7 @@ def clone_request(request, method):
parser_context=request.parser_context) parser_context=request.parser_context)
ret._data = request._data ret._data = request._data
ret._files = request._files ret._files = request._files
ret._full_data = request._full_data
ret._content_type = request._content_type ret._content_type = request._content_type
ret._stream = request._stream ret._stream = request._stream
ret._method = method ret._method = method
@ -133,6 +144,7 @@ class Request(object):
self.parser_context = parser_context self.parser_context = parser_context
self._data = Empty self._data = Empty
self._files = Empty self._files = Empty
self._full_data = Empty
self._method = Empty self._method = Empty
self._content_type = Empty self._content_type = Empty
self._stream = Empty self._stream = Empty
@ -186,12 +198,25 @@ class Request(object):
return self._stream return self._stream
@property @property
def QUERY_PARAMS(self): def query_params(self):
""" """
More semantically correct name for request.GET. More semantically correct name for request.GET.
""" """
return self._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 @property
def DATA(self): def DATA(self):
""" """
@ -272,6 +297,10 @@ class Request(object):
if not _hasattr(self, '_data'): if not _hasattr(self, '_data'):
self._data, self._files = self._parse() 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): 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. # At this point we're committed to parsing the request as form data.
self._data = self._request.POST self._data = self._request.POST
self._files = self._request.FILES 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. # Method overloading - change the method and remove the param from the content.
if ( if (
@ -350,7 +380,7 @@ class Request(object):
): ):
self._content_type = self._data[self._CONTENTTYPE_PARAM] self._content_type = self._data[self._CONTENTTYPE_PARAM]
self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding'])) 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): def _parse(self):
""" """
@ -380,6 +410,7 @@ class Request(object):
# logging the request or similar. # logging the request or similar.
self._data = QueryDict('', encoding=self._request._encoding) self._data = QueryDict('', encoding=self._request._encoding)
self._files = MultiValueDict() self._files = MultiValueDict()
self._full_data = self._data
raise raise
# Parser classes may return the raw data, or a # Parser classes may return the raw data, or a

View File

@ -57,21 +57,24 @@ class BaseSerializer(Field):
def to_representation(self, instance): def to_representation(self, instance):
raise NotImplementedError('`to_representation()` must be implemented.') raise NotImplementedError('`to_representation()` must be implemented.')
def update(self, instance, attrs): def update(self, instance, validated_data):
raise NotImplementedError('`update()` must be implemented.') raise NotImplementedError('`update()` must be implemented.')
def create(self, attrs): def create(self, validated_data):
raise NotImplementedError('`create()` must be implemented.') raise NotImplementedError('`create()` must be implemented.')
def save(self, extras=None): def save(self, extras=None):
attrs = self.validated_data validated_data = self.validated_data
if extras is not None: 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: if self.instance is not None:
self.update(self.instance, attrs) self.update(self.instance, validated_data)
else: else:
self.instance = self.create(attrs) self.instance = self.create(validated_data)
return self.instance return self.instance
@ -321,12 +324,6 @@ class ListSerializer(BaseSerializer):
def create(self, attrs_list): def create(self, attrs_list):
return [self.child.create(attrs) for attrs in 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): def __repr__(self):
return representation.list_repr(self, indent=1) return representation.list_repr(self, indent=1)

View File

@ -9,7 +9,10 @@ import pytest
# Tests for field keyword arguments and core functionality. # Tests for field keyword arguments and core functionality.
# --------------------------------------------------------- # ---------------------------------------------------------
class TestFieldOptions: class TestEmpty:
"""
Tests for `required`, `allow_null`, `allow_blank`, `default`.
"""
def test_required(self): def test_required(self):
""" """
By default a field must be included in the input. By default a field must be included in the input.
@ -69,6 +72,17 @@ class TestFieldOptions:
output = field.run_validation() output = field.run_validation()
assert output is 123 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): def test_redundant_source(self):
class ExampleSerializer(serializers.Serializer): class ExampleSerializer(serializers.Serializer):
example_field = serializers.CharField(source='example_field') 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. # 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()), '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): 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()), 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()) '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 = { invalid_inputs = {

View File

@ -38,7 +38,7 @@ class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
class SlugSerializer(serializers.ModelSerializer): class SlugSerializer(serializers.ModelSerializer):
slug = serializers.Field(read_only=True) slug = serializers.ReadOnlyField()
class Meta: class Meta:
model = SlugBasedModel model = SlugBasedModel

View File

@ -1,4 +1,5 @@
from rest_framework import serializers from rest_framework import serializers
import pytest
# Tests for core functionality. # Tests for core functionality.
@ -29,6 +30,67 @@ class TestSerializer:
assert serializer.validated_data == {'char': 'abc'} assert serializer.validated_data == {'char': 'abc'}
assert serializer.errors == {} 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 -*- # # -*- coding: utf-8 -*-
# from __future__ import unicode_literals # from __future__ import unicode_literals