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
|
# 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`.
|
||||||
|
|
||||||
The `exclude` option is no longer available. You should use the more explicit `fields` option instead.
|
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)
|
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).
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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(',')]
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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(',')]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user