mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 17:47:04 +03:00
request.data attribute
This commit is contained in:
parent
417fe1b675
commit
2859eaf524
|
@ -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).
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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(',')]
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(',')]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,12 +198,25 @@ 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):
|
||||
"""
|
||||
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user