mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 09:36:49 +03:00
commit
b106ebd2c0
|
@ -75,7 +75,7 @@ You can also use the excellent [`tox`][tox] testing tool to run the tests agains
|
|||
|
||||
It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission.
|
||||
|
||||
It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another seperate issue without interfering with an ongoing pull requests.
|
||||
It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another separate issue without interfering with an ongoing pull requests.
|
||||
|
||||
It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests.
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ There is a live example API for testing purposes, [available here][sandbox].
|
|||
# Requirements
|
||||
|
||||
* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4)
|
||||
* Django (1.4.2+, 1.5, 1.6, 1.7)
|
||||
* Django (1.4.11+, 1.5.5+, 1.6, 1.7)
|
||||
|
||||
# Installation
|
||||
|
||||
|
|
|
@ -274,7 +274,27 @@ Corresponds to `django.db.models.fields.FloatField`.
|
|||
|
||||
## DecimalField
|
||||
|
||||
A decimal representation.
|
||||
A decimal representation, represented in Python by a Decimal instance.
|
||||
|
||||
Has two required arguments:
|
||||
|
||||
- `max_digits` The maximum number of digits allowed in the number. Note that this number must be greater than or equal to decimal_places.
|
||||
|
||||
- `decimal_places` The number of decimal places to store with the number.
|
||||
|
||||
For example, to validate numbers up to 999 with a resolution of 2 decimal places, you would use:
|
||||
|
||||
serializers.DecimalField(max_digits=5, decimal_places=2)
|
||||
|
||||
And to validate numbers up to anything less than one billion with a resolution of 10 decimal places:
|
||||
|
||||
serializers.DecimalField(max_digits=19, decimal_places=10)
|
||||
|
||||
This field also takes an optional argument, `coerce_to_string`. If set to `True` the representation will be output as a string. If set to `False` the representation will be left as a `Decimal` instance and the final representation will be determined by the renderer.
|
||||
|
||||
If unset, this will default to the same value as the `COERCE_DECIMAL_TO_STRING` setting, which is `True` unless set otherwise.
|
||||
|
||||
**Signature:** `DecimalField(max_digits, decimal_places, coerce_to_string=None)`
|
||||
|
||||
Corresponds to `django.db.models.fields.DecimalField`.
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ Typically when using the generic views, you'll override the view, and set severa
|
|||
|
||||
from django.contrib.auth.models import User
|
||||
from myapp.serializers import UserSerializer
|
||||
from rest_framework import generics
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework import generics
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
|
||||
class UserList(generics.ListCreateAPIView):
|
||||
queryset = User.objects.all()
|
||||
|
@ -212,8 +212,6 @@ Provides a `.list(request, *args, **kwargs)` method, that implements listing a q
|
|||
|
||||
If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated.
|
||||
|
||||
If the queryset is empty this returns a `200 OK` response, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`.
|
||||
|
||||
## CreateModelMixin
|
||||
|
||||
Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.
|
||||
|
@ -370,6 +368,20 @@ If you are using a mixin across multiple views, you can take this a step further
|
|||
|
||||
Using custom base classes is a good option if you have custom behavior that consistently needs to be repeated across a large number of views throughout your project.
|
||||
|
||||
---
|
||||
|
||||
# PUT as create
|
||||
|
||||
Prior to version 3.0 the REST framework mixins treated `PUT` as either an update or a create operation, depending on if the object already existed or not.
|
||||
|
||||
Allowing `PUT` as create operations is problematic, as it necessarily 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 necessarily 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 from version 3.0 onwards we now use 404 behavior as the default, due to it being simpler and more obvious.
|
||||
|
||||
If you need to generic PUT-as-create behavior you may want to include something like [this `AllowPUTAsCreateMixin` class](https://gist.github.com/tomchristie/a2ace4577eff2c603b1b) as a mixin to your views.
|
||||
|
||||
---
|
||||
|
||||
# Third party packages
|
||||
|
||||
The following third party packages provide additional generic view implementations.
|
||||
|
|
103
docs/api-guide/metadata.md
Normal file
103
docs/api-guide/metadata.md
Normal file
|
@ -0,0 +1,103 @@
|
|||
<a class="github" href="metadata.py"></a>
|
||||
|
||||
# Metadata
|
||||
|
||||
> [The `OPTIONS`] method allows a client to determine the options and/or requirements associated with a resource, or the capabilities of a server, without implying a resource action or initiating a resource retrieval.
|
||||
>
|
||||
> — [RFC7231, Section 4.3.7.][cite]
|
||||
|
||||
REST framework includes a configurable mechanism for determining how your API should respond to `OPTIONS` requests. This allows you to return API schema or other resource information.
|
||||
|
||||
There are not currently any widely adopted conventions for exactly what style of response should be returned for HTTP `OPTIONS` requests, so we provide an ad-hoc style that returns some useful information.
|
||||
|
||||
Here's an example response that demonstrates the information that is returned by default.
|
||||
|
||||
HTTP 200 OK
|
||||
Allow: GET, POST, HEAD, OPTIONS
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "To Do List",
|
||||
"description": "List existing 'To Do' items, or create a new item.",
|
||||
"renders": [
|
||||
"application/json",
|
||||
"text/html"
|
||||
],
|
||||
"parses": [
|
||||
"application/json",
|
||||
"application/x-www-form-urlencoded",
|
||||
"multipart/form-data"
|
||||
],
|
||||
"actions": {
|
||||
"POST": {
|
||||
"note": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"read_only": false,
|
||||
"label": "title",
|
||||
"max_length": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## Setting the metadata scheme
|
||||
|
||||
You can set the metadata class globally using the `'DEFAULT_METADATA_CLASS'` settings key:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata'
|
||||
}
|
||||
|
||||
Or you can set the metadata class individually for a view:
|
||||
|
||||
class APIRoot(APIView):
|
||||
metadata_class = APIRootMetadata
|
||||
|
||||
def get(self, request, format=None):
|
||||
return Response({
|
||||
...
|
||||
})
|
||||
|
||||
The REST framework package only includes a single metadata class implementation, named `SimpleMetadata`. If you want to use an alternative style you'll need to implement a custom metadata class.
|
||||
|
||||
## Creating schema endpoints
|
||||
|
||||
If you have specific requirements for creating schema endpoints that are accessed with regular `GET` requests, you might consider re-using the metadata API for doing so.
|
||||
|
||||
For example, the following additional route could be used on a viewset to provide a linkable schema endpoint.
|
||||
|
||||
@list_route(methods=['GET'])
|
||||
def schema(self, request):
|
||||
meta = self.metadata_class()
|
||||
data = meta.determine_metadata(request, self)
|
||||
return Response(data)
|
||||
|
||||
There are a couple of reasons that you might choose to take this approach, including that `OPTIONS` responses [are not cacheable][no-options].
|
||||
|
||||
---
|
||||
|
||||
# Custom metadata classes
|
||||
|
||||
If you want to provide a custom metadata class you should override `BaseMetadata` and implement the `determine_metadata(self, request, view)` method.
|
||||
|
||||
Useful things that you might want to do could include returning schema information, using a format such as [JSON schema][json-schema], or returning debug information to admin users.
|
||||
|
||||
## Example
|
||||
|
||||
The following class could be used to limit the information that is returned to `OPTIONS` requests.
|
||||
|
||||
class MinimalMetadata(BaseMetadata):
|
||||
"""
|
||||
Don't include field and other information for `OPTIONS` requests.
|
||||
Just return the name and description.
|
||||
"""
|
||||
def determine_metadata(self, request, view):
|
||||
return {
|
||||
'name': view.get_view_name(),
|
||||
'description': view.get_view_description()
|
||||
}
|
||||
|
||||
[cite]: http://tools.ietf.org/html/rfc7231#section-4.3.7
|
||||
[no-options]: https://www.mnot.net/blog/2012/10/29/NO_OPTIONS
|
||||
[json-schema]: http://json-schema.org/
|
|
@ -74,37 +74,18 @@ If your API includes views that can serve both regular webpages and API response
|
|||
|
||||
Renders the request data into `JSON`, using utf-8 encoding.
|
||||
|
||||
Note that non-ascii characters will be rendered using JSON's `\uXXXX` character escape. For example:
|
||||
Note that the default style is to include unicode characters, and render the response using a compact style with no unnecessary whitespace:
|
||||
|
||||
{"unicode black star": "\u2605"}
|
||||
{"unicode black star":"★","value":999}
|
||||
|
||||
The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
|
||||
|
||||
{
|
||||
"unicode black star": "\u2605"
|
||||
"unicode black star": "★",
|
||||
"value": 999
|
||||
}
|
||||
|
||||
**.media_type**: `application/json`
|
||||
|
||||
**.format**: `'.json'`
|
||||
|
||||
**.charset**: `None`
|
||||
|
||||
## UnicodeJSONRenderer
|
||||
|
||||
Renders the request data into `JSON`, using utf-8 encoding.
|
||||
|
||||
Note that non-ascii characters will not be character escaped. For example:
|
||||
|
||||
{"unicode black star": "★"}
|
||||
|
||||
The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
|
||||
|
||||
{
|
||||
"unicode black star": "★"
|
||||
}
|
||||
|
||||
Both the `JSONRenderer` and `UnicodeJSONRenderer` styles conform to [RFC 4627][rfc4627], and are syntactically valid JSON.
|
||||
The default JSON encoding style can be altered using the `UNICODE_JSON` and `COMPACT_JSON` settings keys.
|
||||
|
||||
**.media_type**: `application/json`
|
||||
|
||||
|
|
|
@ -265,7 +265,7 @@ A format string that should be used by default for rendering the output of `Date
|
|||
|
||||
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
|
||||
|
||||
Default: `None`
|
||||
Default: `'iso-8601'`
|
||||
|
||||
#### DATETIME_INPUT_FORMATS
|
||||
|
||||
|
@ -281,7 +281,7 @@ A format string that should be used by default for rendering the output of `Date
|
|||
|
||||
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
|
||||
|
||||
Default: `None`
|
||||
Default: `'iso-8601'`
|
||||
|
||||
#### DATE_INPUT_FORMATS
|
||||
|
||||
|
@ -297,7 +297,7 @@ A format string that should be used by default for rendering the output of `Time
|
|||
|
||||
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
|
||||
|
||||
Default: `None`
|
||||
Default: `'iso-8601'`
|
||||
|
||||
#### TIME_INPUT_FORMATS
|
||||
|
||||
|
@ -309,6 +309,46 @@ Default: `['iso-8601']`
|
|||
|
||||
---
|
||||
|
||||
## Encodings
|
||||
|
||||
#### UNICODE_JSON
|
||||
|
||||
When set to `True`, JSON responses will allow unicode characters in responses. For example:
|
||||
|
||||
{"unicode black star":"★"}
|
||||
|
||||
When set to `False`, JSON responses will escape non-ascii characters, like so:
|
||||
|
||||
{"unicode black star":"\u2605"}
|
||||
|
||||
Both styles conform to [RFC 4627][rfc4627], and are syntactically valid JSON. The unicode style is prefered as being more user-friendly when inspecting API responses.
|
||||
|
||||
Default: `True`
|
||||
|
||||
#### COMPACT_JSON
|
||||
|
||||
When set to `True`, JSON responses will return compact representations, with no spacing after `':'` and `','` characters. For example:
|
||||
|
||||
{"is_admin":false,"email":"jane@example"}
|
||||
|
||||
When set to `False`, JSON responses will return slightly more verbose representations, like so:
|
||||
|
||||
{"is_admin": false, "email": "jane@example"}
|
||||
|
||||
The default style is to return minified responses, in line with [Heroku's API design guidelines][heroku-minified-json].
|
||||
|
||||
Default: `True`
|
||||
|
||||
#### COERCE_DECIMAL_TO_STRING
|
||||
|
||||
When returning decimal objects in API representations that do not support a native decimal type, it is normally best to return the value as a string. This avoids the loss of precision that occurs with binary floating point implementations.
|
||||
|
||||
When set to `True`, the serializer `DecimalField` class will return strings instead of `Decimal` objects. When set to `False`, serializers will return `Decimal` objects, which the default JSON encoder will return as floats.
|
||||
|
||||
Default: `True`
|
||||
|
||||
---
|
||||
|
||||
## View names and descriptions
|
||||
|
||||
**The following settings are used to generate the view names and descriptions, as used in responses to `OPTIONS` requests, and as used in the browsable API.**
|
||||
|
@ -378,4 +418,6 @@ An integer of 0 or more, that may be used to specify the number of application p
|
|||
Default: `None`
|
||||
|
||||
[cite]: http://www.python.org/dev/peps/pep-0020/
|
||||
[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
|
||||
[heroku-minified-json]: https://github.com/interagent/http-api-design#keep-json-minified-in-all-responses
|
||||
[strftime]: http://docs.python.org/2/library/time.html#time.strftime
|
||||
|
|
|
@ -178,6 +178,8 @@ To create a custom throttle, override `BaseThrottle` and implement `.allow_reque
|
|||
|
||||
Optionally you may also override the `.wait()` method. If implemented, `.wait()` should return a recommended number of seconds to wait before attempting the next request, or `None`. The `.wait()` method will only be called if `.allow_request()` has previously returned `False`.
|
||||
|
||||
If the `.wait()` method is implemented and the request is throttled, then a `Retry-After` header will be included in the response.
|
||||
|
||||
## Example
|
||||
|
||||
The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests.
|
||||
|
|
183
docs/api-guide/validators.md
Normal file
183
docs/api-guide/validators.md
Normal file
|
@ -0,0 +1,183 @@
|
|||
<a class="github" href="validators.py"></a>
|
||||
|
||||
# Validators
|
||||
|
||||
> Validators can be useful for re-using validation logic between different types of fields.
|
||||
>
|
||||
> — [Django documentation][cite]
|
||||
|
||||
Most of the time you're dealing with validation in REST framework you'll simply be relying on the default field validation, or writing explicit validation methods on serializer or field classes.
|
||||
|
||||
However, sometimes you'll want to place your validation logic into reusable components, so that it can easily be reused throughout your codebase. This can be achieved by using validator functions and validator classes.
|
||||
|
||||
## Validation in REST framework
|
||||
|
||||
Validation in Django REST framework serializers is handled a little differently to how validation works in Django's `ModelForm` class.
|
||||
|
||||
With `ModelForm` the validation is performed partially on the form, and partially on the model instance. With REST framework the validation is performed entirely on the serializer class. This is advantageous for the following reasons:
|
||||
|
||||
* It introduces a proper separation of concerns, making your code behaviour more obvious.
|
||||
* It is easy to switch between using shortcut `ModelSerializer` classes and using explicit `Serializer` classes. Any validation behaviour being used for `ModelSerializer` is simple to replicate.
|
||||
* Printing the `repr` of a serializer instance will show you exactly what validation rules it applies. There's no extra hidden validation behaviour being called on the model instance.
|
||||
|
||||
When you're using `ModelSerializer` all of this is handled automatically for you. If you want to drop down to using a `Serializer` classes instead, then you need to define the validation rules explicitly.
|
||||
|
||||
#### Example
|
||||
|
||||
As an example of how REST framework uses explicit validation, we'll take a simple model class that has a field with a uniqueness constraint.
|
||||
|
||||
class CustomerReportRecord(models.Model):
|
||||
time_raised = models.DateTimeField(default=timezone.now, editable=False)
|
||||
reference = models.CharField(unique=True, max_length=20)
|
||||
description = models.TextField()
|
||||
|
||||
Here's a basic `ModelSerializer` that we can use for creating or updating instances of `CustomerReportRecord`:
|
||||
|
||||
class CustomerReportSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CustomerReportRecord
|
||||
|
||||
If we open up the Django shell using `manage.py shell` we can now
|
||||
|
||||
>>> from project.example.serializers import CustomerReportSerializer
|
||||
>>> serializer = CustomerReportSerializer()
|
||||
>>> print(repr(serializer))
|
||||
CustomerReportSerializer():
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
time_raised = DateTimeField(read_only=True)
|
||||
reference = CharField(max_length=20, validators=[<UniqueValidator(queryset=CustomerReportRecord.objects.all())>])
|
||||
description = CharField(style={'type': 'textarea'})
|
||||
|
||||
The interesting bit here is the `reference` field. We can see that the uniqueness constraint is being explicitly enforced by a validator on the serializer field.
|
||||
|
||||
Because of this more explicit style REST framework includes a few validator classes that are not available in core Django. These classes are detailed below.
|
||||
|
||||
---
|
||||
|
||||
## UniqueValidator
|
||||
|
||||
This validator can be used to enforce the `unique=True` constraint on model fields.
|
||||
It takes a single required argument, and an optional `messages` argument:
|
||||
|
||||
* `queryset` *required* - This is the queryset against which uniqueness should be enforced.
|
||||
* `message` - The error message that should be used when validation fails.
|
||||
|
||||
This validator should be applied to *serializer fields*, like so:
|
||||
|
||||
slug = SlugField(
|
||||
max_length=100,
|
||||
validators=[UniqueValidator(queryset=BlogPost.objects.all())]
|
||||
)
|
||||
|
||||
## UniqueTogetherValidator
|
||||
|
||||
This validator can be used to enforce `unique_together` constraints on model instances.
|
||||
It has two required arguments, and a single optional `messages` argument:
|
||||
|
||||
* `queryset` *required* - This is the queryset against which uniqueness should be enforced.
|
||||
* `fields` *required* - A list or tuple of field names which should make a unique set. These must exist as fields on the serializer class.
|
||||
* `message` - The error message that should be used when validation fails.
|
||||
|
||||
The validator should be applied to *serializer classes*, like so:
|
||||
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
# ...
|
||||
class Meta:
|
||||
# ToDo items belong to a parent list, and have an ordering defined
|
||||
# by the 'position' field. No two items in a given list may share
|
||||
# the same position.
|
||||
validators = [
|
||||
UniqueTogetherValidator(
|
||||
queryset=ToDoItem.objects.all(),
|
||||
fields=('list', 'position')
|
||||
)
|
||||
]
|
||||
|
||||
## UniqueForDateValidator
|
||||
|
||||
## UniqueForMonthValidator
|
||||
|
||||
## UniqueForYearValidator
|
||||
|
||||
These validators can be used to enforce the `unique_for_date`, `unique_for_month` and `unique_for_year` constraints on model instances. They take the following arguments:
|
||||
|
||||
* `queryset` *required* - This is the queryset against which uniqueness should be enforced.
|
||||
* `field` *required* - A field name against which uniqueness in the given date range will be validated. This must exist as a field on the serializer class.
|
||||
* `date_field` *required* - A field name which will be used to determine date range for the uniqueness constrain. This must exist as a field on the serializer class.
|
||||
* `message` - The error message that should be used when validation fails.
|
||||
|
||||
The validator should be applied to *serializer classes*, like so:
|
||||
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
# ...
|
||||
class Meta:
|
||||
# Blog posts should have a slug that is unique for the current year.
|
||||
validators = [
|
||||
UniqueForYearValidator(
|
||||
queryset=BlogPostItem.objects.all(),
|
||||
field='slug',
|
||||
date_field='published'
|
||||
)
|
||||
]
|
||||
|
||||
The date field that is used for the validation is always required to be present on the serializer class. You can't simply rely on a model class `default=...`, because the value being used for the default wouldn't be generated until after the validation has run.
|
||||
|
||||
There are a couple of styles you may want to use for this depending on how you want your API to behave. If you're using `ModelSerializer` you'll probably simply rely on the defaults that REST framework generates for you, but if you are using `Serializer` or simply want more explicit control, use on of the styles demonstrated below.
|
||||
|
||||
#### Using with a writable date field.
|
||||
|
||||
If you want the date field to be writable the only thing worth noting is that you should ensure that it is always available in the input data, either by setting a `default` argument, or by setting `required=True`.
|
||||
|
||||
published = serializers.DateTimeField(required=True)
|
||||
|
||||
#### Using with a read-only date field.
|
||||
|
||||
If you want the date field to be visible, but not editable by the user, then set `read_only=True` and additionally set a `default=...` argument.
|
||||
|
||||
published = serializers.DateTimeField(read_only=True, default=timezone.now)
|
||||
|
||||
The field will not be writable to the user, but the default value will still be passed through to the `validated_data`.
|
||||
|
||||
#### Using with a hidden date field.
|
||||
|
||||
If you want the date field to be entirely hidden from the user, then use `HiddenField`. This field type does not accept user input, but instead always returns it's default value to the `validated_data` in the serializer.
|
||||
|
||||
published = serializers.HiddenField(default=timezone.now)
|
||||
|
||||
---
|
||||
|
||||
# Writing custom validators
|
||||
|
||||
You can use any of Django's existing validators, or write your own custom validators.
|
||||
|
||||
## Function based
|
||||
|
||||
A validator may be any callable that raises a `serializers.ValidationError` on failure.
|
||||
|
||||
def even_number(value):
|
||||
if value % 2 != 0:
|
||||
raise serializers.ValidationError('This field must be an even number.')
|
||||
|
||||
## Class based
|
||||
|
||||
To write a class based validator, use the `__call__` method. Class based validators are useful as they allow you to parameterize and reuse behavior.
|
||||
|
||||
class MultipleOf:
|
||||
def __init__(self, base):
|
||||
self.base = base
|
||||
|
||||
def __call__(self, value):
|
||||
if value % self.base != 0
|
||||
message = 'This field must be a multiple of %d.' % self.base
|
||||
raise serializers.ValidationError(message)
|
||||
|
||||
#### Using `set_context()`
|
||||
|
||||
In some advanced cases you might want a validator to be passed the serializer field it is being used with as additional context. You can do so by declaring a `set_context` method on a class based validator.
|
||||
|
||||
def set_context(self, serializer_field):
|
||||
# Determine if this is an update or a create operation.
|
||||
# In `__call__` we can then use that information to modify the validation behavior.
|
||||
self.is_update = serializer_field.parent.instance is not None
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/ref/validators/
|
|
@ -50,7 +50,7 @@ Some reasons you might want to use REST framework:
|
|||
REST framework requires the following:
|
||||
|
||||
* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4)
|
||||
* Django (1.4.2+, 1.5, 1.6, 1.7)
|
||||
* Django (1.4.11+, 1.5.5+, 1.6, 1.7)
|
||||
|
||||
The following packages are optional:
|
||||
|
||||
|
@ -173,6 +173,7 @@ The API guide is your complete reference manual to all the functionality provide
|
|||
* [Serializers][serializers]
|
||||
* [Serializer fields][fields]
|
||||
* [Serializer relations][relations]
|
||||
* [Validators][validators]
|
||||
* [Authentication][authentication]
|
||||
* [Permissions][permissions]
|
||||
* [Throttling][throttling]
|
||||
|
@ -294,6 +295,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[serializers]: api-guide/serializers.md
|
||||
[fields]: api-guide/fields.md
|
||||
[relations]: api-guide/relations.md
|
||||
[validation]: api-guide/validation.md
|
||||
[authentication]: api-guide/authentication.md
|
||||
[permissions]: api-guide/permissions.md
|
||||
[throttling]: api-guide/throttling.md
|
||||
|
|
|
@ -95,6 +95,7 @@ a.fusion-poweredby {
|
|||
<li><a href="{{ base_url }}/api-guide/serializers{{ suffix }}">Serializers</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/fields{{ suffix }}">Serializer fields</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/relations{{ suffix }}">Serializer relations</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/validators{{ suffix }}">Validators</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/authentication{{ suffix }}">Authentication</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/permissions{{ suffix }}">Permissions</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/throttling{{ suffix }}">Throttling</a></li>
|
||||
|
|
|
@ -23,7 +23,7 @@ The documentation has previously stated that usage of the more explicit style is
|
|||
|
||||
Doing so will mean that there are cases of API code where you'll now need to include a serializer class where you previously were just using the `.model` shortcut. However we firmly believe that it is the right trade-off to make.
|
||||
|
||||
Removing the shortcut takes away an unneccessary layer of abstraction, and makes your codebase more explicit without any significant extra complexity. It also results in better consistency, as there's now only one way to set the serializer class and queryset attributes for the view, instead of two.
|
||||
Removing the shortcut takes away an unnecessary layer of abstraction, and makes your codebase more explicit without any significant extra complexity. It also results in better consistency, as there's now only one way to set the serializer class and queryset attributes for the view, instead of two.
|
||||
|
||||
The `DEFAULT_MODEL_SERIALIZER_CLASS` API setting is now also deprecated.
|
||||
|
||||
|
|
816
docs/topics/3.0-announcement.md
Normal file
816
docs/topics/3.0-announcement.md
Normal file
|
@ -0,0 +1,816 @@
|
|||
## Pre-release notes:
|
||||
|
||||
The 3.0 release is now ready for some tentative testing and upgrades for super keen early adopters. You can install the development version directly from GitHub like so:
|
||||
|
||||
pip install https://github.com/tomchristie/django-rest-framework/archive/version-3.0.zip
|
||||
|
||||
See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-framework/pull/1800) for more details on remaining work.
|
||||
|
||||
The most notable outstanding issues still to be resolved on the `version-3.0` branch are as follows:
|
||||
|
||||
* Finish forms support for serializers and in the browsable API.
|
||||
* Optimisations for serializing primary keys.
|
||||
* Refine style of validation errors in some cases, such as validation errors in `ListField`.
|
||||
|
||||
**Your feedback on the upgrade process and 3.0 changes is hugely important!**
|
||||
|
||||
Please do get in touch via twitter, IRC, a GitHub ticket, or the discussion group.
|
||||
|
||||
---
|
||||
|
||||
# REST framework 3.0
|
||||
|
||||
The 3.0 release of Django REST framework is the result of almost four years of iteration and refinement. It comprehensively addresses some of the previous remaining design issues in serializers, fields and the generic views.
|
||||
|
||||
This release is incremental in nature. There *are* some breaking API changes, and upgrading *will* require you to read the release notes carefully, but the migration path should otherwise be relatively straightforward.
|
||||
|
||||
The difference in quality of the REST framework API and implementation should make writing, maintaining and debugging your application far easier.
|
||||
|
||||
## New features
|
||||
|
||||
Notable features of this new release include:
|
||||
|
||||
* Printable representations on serializers that allow you to inspect exactly what fields are present on the instance.
|
||||
* Simple model serializers that are vastly easier to understand and debug, and that make it easy to switch between the implicit `ModelSerializer` class and the explicit `Serializer` class.
|
||||
* A new `BaseSerializer` class, making it easier to write serializers for alternative storage backends, or to completely customize your serialization and validation logic.
|
||||
* A cleaner fields API plus new `ListField` and `MultipleChoiceField` classes.
|
||||
* Super simple default implementations for the generic views.
|
||||
* Support for overriding how validation errors are handled by your API.
|
||||
* A metadata API that allows you to customize how `OPTIONS` requests are handled by your API.
|
||||
* A more compact JSON output with unicode style encoding turned on by default.
|
||||
|
||||
Below is an in-depth guide to the API changes and migration notes for 3.0.
|
||||
|
||||
---
|
||||
|
||||
## Request objects
|
||||
|
||||
#### The `.data` and `.query_params` properties.
|
||||
|
||||
The usage of `request.DATA` and `request.FILES` is now pending deprecation in favor of a single `request.data` attribute that contains *all* the parsed data.
|
||||
|
||||
Having separate 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.
|
||||
|
||||
You may now pass all the request data to a serializer class in a single argument:
|
||||
|
||||
# Do this...
|
||||
ExampleSerializer(data=request.data)
|
||||
|
||||
Instead of passing the files argument separately:
|
||||
|
||||
# Don't do this...
|
||||
ExampleSerializer(data=request.DATA, files=request.FILES)
|
||||
|
||||
|
||||
The usage of `request.QUERY_PARAMS` is now pending deprecation in favor of the lowercased `request.query_params`.
|
||||
|
||||
## Serializers
|
||||
|
||||
#### Single-step object creation.
|
||||
|
||||
Previously the serializers used a two-step object creation, as follows:
|
||||
|
||||
1. Validating the data would create an object instance. This instance would be available as `serializer.object`.
|
||||
2. Calling `serializer.save()` would then save the object instance to the database.
|
||||
|
||||
This style is in-line with how the `ModelForm` class works in Django, but is problematic for a number of reasons:
|
||||
|
||||
* Some data, such as many-to-many relationships, cannot be added to the object instance until after it has been saved. This type of data needed to be hidden in some undocumented state on the object instance, or kept as state on the serializer instance so that it could be used when `.save()` is called.
|
||||
* Instantiating model instances directly means that you cannot use model manager classes for instance creation, eg `ExampleModel.objects.create(...)`. Manager classes are an excellent layer at which to enforce business logic and application-level data constraints.
|
||||
* The two step process makes it unclear where to put deserialization logic. For example, should extra attributes such as the current user get added to the instance during object creation or during object save?
|
||||
|
||||
We now use single-step object creation, like so:
|
||||
|
||||
1. Validating the data makes the cleaned data available as `serializer.validated_data`.
|
||||
2. Calling `serializer.save()` then saves and returns the new object instance.
|
||||
|
||||
The resulting API changes are further detailed below.
|
||||
|
||||
#### The `.create()` and `.update()` methods.
|
||||
|
||||
The `.restore_object()` method is now replaced with two separate methods, `.create()` and `.update()`.
|
||||
|
||||
When using the `.create()` and `.update()` methods you should both create *and save* the object instance. This is in contrast to the previous `.restore_object()` behavior that would instantiate the object but not save it.
|
||||
|
||||
The following example from the tutorial previously used `restore_object()` to handle both creating and updating object instances.
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
if instance:
|
||||
# Update existing instance
|
||||
instance.title = attrs.get('title', instance.title)
|
||||
instance.code = attrs.get('code', instance.code)
|
||||
instance.linenos = attrs.get('linenos', instance.linenos)
|
||||
instance.language = attrs.get('language', instance.language)
|
||||
instance.style = attrs.get('style', instance.style)
|
||||
return instance
|
||||
|
||||
# Create new instance
|
||||
return Snippet(**attrs)
|
||||
|
||||
This would now be split out into two separate methods.
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
instance.title = validated_data.get('title', instance.title)
|
||||
instance.code = validated_data.get('code', instance.code)
|
||||
instance.linenos = validated_data.get('linenos', instance.linenos)
|
||||
instance.language = validated_data.get('language', instance.language)
|
||||
instance.style = validated_data.get('style', instance.style)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def create(self, validated_data):
|
||||
return Snippet.objects.create(**validated_data)
|
||||
|
||||
Note that these methods should return the newly created object instance.
|
||||
|
||||
#### Use `.validated_data` instead of `.object`.
|
||||
|
||||
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.
|
||||
|
||||
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 as keyword arguments.
|
||||
|
||||
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)
|
||||
serializer.save(user=request.user) # Include the user when saving.
|
||||
|
||||
#### Using `serializers.ValidationError`.
|
||||
|
||||
Previously `serializers.ValidationError` error was simply a synonym for `django.core.exceptions.ValidationError`. This has now been altered so that it inherits from the standard `APIException` base class.
|
||||
|
||||
The reason behind this is that Django's `ValidationError` class is intended for use with HTML forms and its API makes using it slightly awkward with nested validation errors that can occur in serializers.
|
||||
|
||||
For most users this change shouldn't require any updates to your codebase, but it is worth ensuring that whenever raising validation errors you are always using the `serializers.ValidationError` exception class, and not Django's built-in exception.
|
||||
|
||||
We strongly recommend that you use the namespaced import style of `import serializers` and not `from serializers import ValidationError` in order to avoid any potential confusion.
|
||||
|
||||
#### Change to `validate_<field_name>`.
|
||||
|
||||
The `validate_<field_name>` method hooks that can be attached to serializer classes change their signature slightly and return type. Previously these would take a dictionary of all incoming data, and a key representing the field name, and would return a dictionary including the validated data for that field:
|
||||
|
||||
def validate_score(self, attrs, source):
|
||||
if attrs[score] % 10 != 0:
|
||||
raise serializers.ValidationError('This field should be a multiple of ten.')
|
||||
return attrs
|
||||
|
||||
This is now simplified slightly, and the method hooks simply take the value to be validated, and return the validated value.
|
||||
|
||||
def validate_score(self, value):
|
||||
if value % 10 != 0:
|
||||
raise serializers.ValidationError('This field should be a multiple of ten.')
|
||||
return value
|
||||
|
||||
Any ad-hoc validation that applies to more than one field should go in the `.validate(self, attrs)` method as usual.
|
||||
|
||||
Because `.validate_<field_name>` would previously accept the complete dictionary of attributes, it could be used to validate a field depending on the input in another field. Now if you need to do this you should use `.validate()` instead.
|
||||
|
||||
You can either return `non_field_errors` from the validate method by raising a simple `ValidationError`
|
||||
|
||||
def validate(self, attrs):
|
||||
# serializer.errors == {'non_field_errors': ['A non field error']}
|
||||
raise serailizers.ValidationError('A non field error')
|
||||
|
||||
Alternatively if you want the errors to be against a specific field, use a dictionary of when instantiating the `ValidationError`, like so:
|
||||
|
||||
def validate(self, attrs):
|
||||
# serializer.errors == {'my_field': ['A field error']}
|
||||
raise serailizers.ValidationError({'my_field': 'A field error'})
|
||||
|
||||
This ensures you can still write validation that compares all the input fields, but that marks the error against a particular field.
|
||||
|
||||
#### Limitations of ModelSerializer validation.
|
||||
|
||||
This change also means that we no longer use the `.full_clean()` method on model instances, but instead perform all validation explicitly on the serializer. This gives a cleaner separation, and ensures that there's no automatic validation behavior on `ModelSerializer` classes that can't also be easily replicated on regular `Serializer` classes.
|
||||
|
||||
This change comes with the following limitations:
|
||||
|
||||
* The model `.clean()` method will not be called as part of serializer validation. Use the serializer `.validate()` method to perform a final validation step on incoming data where required.
|
||||
* The `.unique_for_date`, `.unique_for_month` and `.unique_for_year` options on model fields are not automatically validated. Again, you'll need to handle these explicitly on the serializer if required.
|
||||
|
||||
#### Writable nested serialization.
|
||||
|
||||
REST framework 2.x attempted to automatically support writable nested serialization, but the behavior was complex and non-obvious. Attempting to automatically handle these case is problematic:
|
||||
|
||||
* There can be complex dependencies involved in order of saving multiple related model instances.
|
||||
* It's unclear what behavior the user should expect when related models are passed `None` data.
|
||||
* It's unclear how the user should expect to-many relationships to handle updates, creations and deletions of multiple records.
|
||||
|
||||
Using the `depth` option on `ModelSerializer` will now create **read-only nested serializers** by default.
|
||||
|
||||
If you try to use a writable nested serializer without writing a custom `create()` and/or `update()` method you'll see an assertion error when you attempt to save the serializer. For example:
|
||||
|
||||
>>> class ProfileSerializer(serializers.ModelSerializer):
|
||||
>>> class Meta:
|
||||
>>> model = Profile
|
||||
>>> fields = ('address', 'phone')
|
||||
>>>
|
||||
>>> class UserSerializer(serializers.ModelSerializer):
|
||||
>>> profile = ProfileSerializer()
|
||||
>>> class Meta:
|
||||
>>> model = User
|
||||
>>> fields = ('username', 'email', 'profile')
|
||||
>>>
|
||||
>>> data = {
|
||||
>>> 'username': 'lizzy',
|
||||
>>> 'email': 'lizzy@example.com',
|
||||
>>> 'profile': {'address': '123 Acacia Avenue', 'phone': '01273 100200'}
|
||||
>>> }
|
||||
>>>
|
||||
>>> serializer = UserSerializer(data=data)
|
||||
>>> serializer.save()
|
||||
AssertionError: The `.create()` method does not suport nested writable fields by default. Write an explicit `.create()` method for serializer `UserSerializer`, or set `read_only=True` on nested serializer fields.
|
||||
|
||||
To use writable nested serialization you'll want to declare a nested field on the serializer class, and write the `create()` and/or `update()` methods explicitly.
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
profile = ProfileSerializer()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('username', 'email', 'profile')
|
||||
|
||||
def create(self, validated_data):
|
||||
profile_data = validated_data.pop['profile']
|
||||
user = User.objects.create(**validated_data)
|
||||
Profile.objects.create(user=user, **profile_data)
|
||||
return user
|
||||
|
||||
The single-step object creation makes this far simpler and more obvious than the previous `.restore_object()` behavior.
|
||||
|
||||
#### Printable serializer representations.
|
||||
|
||||
Serializer instances now support a printable representation that allows you to inspect the fields present on the instance.
|
||||
|
||||
For instance, given the following example model:
|
||||
|
||||
class LocationRating(models.Model):
|
||||
location = models.CharField(max_length=100)
|
||||
rating = models.IntegerField()
|
||||
created_by = models.ForeignKey(User)
|
||||
|
||||
Let's create a simple `ModelSerializer` class corresponding to the `LocationRating` model.
|
||||
|
||||
class LocationRatingSerializer(serializer.ModelSerializer):
|
||||
class Meta:
|
||||
model = LocationRating
|
||||
|
||||
We can now inspect the serializer representation in the Django shell, using `python manage.py shell`...
|
||||
|
||||
>>> serializer = LocationRatingSerializer()
|
||||
>>> print(serializer) # Or use `print serializer` in Python 2.x
|
||||
LocationRatingSerializer():
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
location = CharField(max_length=100)
|
||||
rating = IntegerField()
|
||||
created_by = PrimaryKeyRelatedField(queryset=User.objects.all())
|
||||
|
||||
#### The `extra_kwargs` option.
|
||||
|
||||
The `write_only_fields` option on `ModelSerializer` has been moved to `PendingDeprecation` and replaced with a more generic `extra_kwargs`.
|
||||
|
||||
class MySerializer(serializer.ModelSerializer):
|
||||
class Meta:
|
||||
model = MyModel
|
||||
fields = ('id', 'email', 'notes', 'is_admin')
|
||||
extra_kwargs = {
|
||||
'is_admin': {'write_only': True}
|
||||
}
|
||||
|
||||
Alternatively, specify the field explicitly on the serializer class:
|
||||
|
||||
class MySerializer(serializer.ModelSerializer):
|
||||
is_admin = serializers.BooleanField(write_only=True)
|
||||
|
||||
class Meta:
|
||||
model = MyModel
|
||||
fields = ('id', 'email', 'notes', 'is_admin')
|
||||
|
||||
The `read_only_fields` option remains as a convenient shortcut for the more common case.
|
||||
|
||||
#### Changes to `HyperlinkedModelSerializer`.
|
||||
|
||||
The `view_name` and `lookup_field` options have been moved to `PendingDeprecation`. They are no longer required, as you can use the `extra_kwargs` argument instead:
|
||||
|
||||
class MySerializer(serializer.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = MyModel
|
||||
fields = ('url', 'email', 'notes', 'is_admin')
|
||||
extra_kwargs = {
|
||||
'url': {'lookup_field': 'uuid'}
|
||||
}
|
||||
|
||||
Alternatively, specify the field explicitly on the serializer class:
|
||||
|
||||
class MySerializer(serializer.HyperlinkedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='mymodel-detail',
|
||||
lookup_field='uuid'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = MyModel
|
||||
fields = ('url', 'email', 'notes', 'is_admin')
|
||||
|
||||
#### Fields for model methods and properties.
|
||||
|
||||
With `ModelSerializer` you can now specify field names in the `fields` option that refer to model methods or properties. For example, suppose you have the following model:
|
||||
|
||||
class Invitation(models.Model):
|
||||
created = models.DateTimeField()
|
||||
to_email = models.EmailField()
|
||||
message = models.CharField(max_length=1000)
|
||||
|
||||
def expiry_date(self):
|
||||
return self.created + datetime.timedelta(days=30)
|
||||
|
||||
You can include `expiry_date` as a field option on a `ModelSerializer` class.
|
||||
|
||||
class InvitationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Invitation
|
||||
fields = ('to_email', 'message', 'expiry_date')
|
||||
|
||||
These fields will be mapped to `serializers.ReadOnlyField()` instances.
|
||||
|
||||
>>> serializer = InvitationSerializer()
|
||||
>>> print repr(serializer)
|
||||
InvitationSerializer():
|
||||
to_email = EmailField(max_length=75)
|
||||
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 separated 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 primitive 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 `BaseSerializer` classes.
|
||||
|
||||
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 primitive 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 `BaseSerializer` classes.
|
||||
|
||||
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 generic serializers 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 alternative storage backends.
|
||||
|
||||
The following class is an example of a generic serializer that can handle coercing aribitrary objects into primitive representations.
|
||||
|
||||
class ObjectSerializer(serializers.BaseSerializer):
|
||||
"""
|
||||
A read-only serializer that coerces arbitrary complex objects
|
||||
into primitive 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))):
|
||||
# Primitive 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.
|
||||
|
||||
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.
|
||||
* `WritableField` 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 explicit and clear control over validating empty values for fields.
|
||||
|
||||
Previously the meaning of the `required=False` keyword argument was underspecified. In practice its 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 separation, with separate `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.
|
||||
|
||||
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 `ListField` class.
|
||||
|
||||
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 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.
|
||||
|
||||
The `from_native(self, value)` and `to_native(self, data)` method names have been replaced with the more obviously named `to_internal_value(self, data)` and `to_representation(self, value)`.
|
||||
|
||||
The `field_from_native()` and `field_to_native()` methods are removed.
|
||||
|
||||
#### 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 implicit `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`.
|
||||
|
||||
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 unnecessarily 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')
|
||||
|
||||
#### The `UniqueValidator` and `UniqueTogetherValidator` classes.
|
||||
|
||||
REST framework now provides two new validators that allow you to ensure field uniqueness, while still using a completely explicit `Serializer` class instead of using `ModelSerializer`.
|
||||
|
||||
The `UniqueValidator` should be applied to a serializer field, and takes a single `queryset` argument.
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueValidator
|
||||
|
||||
class OrganizationSerializer(serializers.Serializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='organisation_detail')
|
||||
created = serializers.DateTimeField(read_only=True)
|
||||
name = serializers.CharField(
|
||||
max_length=100,
|
||||
validators=UniqueValidator(queryset=Organisation.objects.all())
|
||||
)
|
||||
|
||||
The `UniqueTogetherValidator` should be applied to a serializer, and takes a `queryset` argument and a `fields` argument which should be a list or tuple of field names.
|
||||
|
||||
class RaceResultSerializer(serializers.Serializer):
|
||||
category = serializers.ChoiceField(['5k', '10k'])
|
||||
position = serializers.IntegerField()
|
||||
name = serializers.CharField(max_length=100)
|
||||
|
||||
default_validators = [UniqueTogetherValidator(
|
||||
queryset=RaceResult.objects.all(),
|
||||
fields=('category', 'position')
|
||||
)]
|
||||
|
||||
## Generic views
|
||||
|
||||
#### Simplification of view logic.
|
||||
|
||||
The view logic for the default method handlers has been significantly simplified, due to the new serializers API.
|
||||
|
||||
#### Changes to pre/post save hooks.
|
||||
|
||||
The `pre_save` and `post_save` hooks no longer exist, but are replaced with `perform_create(self, serializer)` and `perform_update(self, serializer)`.
|
||||
|
||||
These methods should save the object instance by calling `serializer.save()`, adding in any additional arguments as required. They may also perform any custom pre-save or post-save behavior.
|
||||
|
||||
For example:
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# Include the owner attribute directly, rather than from request data.
|
||||
instance = serializer.save(owner=self.request.user)
|
||||
# Perform a custom post-save action.
|
||||
send_email(instance.to_email, instance.message)
|
||||
|
||||
The `pre_delete` and `post_delete` hooks no longer exist, and are replaced with `.perform_destroy(self, instance)`, which should delete the instance and perform any custom actions.
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
# Perform a custom pre-delete action.
|
||||
send_deletion_alert(user=instance.created_by, deleted=instance)
|
||||
# Delete the object instance.
|
||||
instance.delete()
|
||||
|
||||
#### Removal of view attributes.
|
||||
|
||||
The `.object` and `.object_list` attributes are no longer set on the view instance. Treating views as mutable object instances that store state during the processing of the view tends to be poor design, and can lead to obscure flow logic.
|
||||
|
||||
I would personally recommend that developers treat view instances as immutable objects in their application code.
|
||||
|
||||
#### PUT as create.
|
||||
|
||||
Allowing `PUT` as create operations is problematic, as it necessarily 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 necessarily 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 may want to include [this `AllowPUTAsCreateMixin` class](https://gist.github.com/tomchristie/a2ace4577eff2c603b1b) as a mixin to your views.
|
||||
|
||||
#### Customizing error responses.
|
||||
|
||||
The generic views now raise `ValidationFailed` exception 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 customize the style of error responses across your entire API, without having to modify any of the generic views.
|
||||
|
||||
## The metadata API
|
||||
|
||||
Behavior for dealing with `OPTIONS` requests was previously built directly into the class based views. This has now been properly separated out into a Metadata API that allows the same pluggable style as other API policies in REST framework.
|
||||
|
||||
This makes it far easier to use a different style for `OPTIONS` responses throughout your API, and makes it possible to create third-party metadata policies.
|
||||
|
||||
## API style
|
||||
|
||||
There are some improvements in the default style we use in our API responses.
|
||||
|
||||
#### Unicode JSON by default.
|
||||
|
||||
Unicode JSON is now the default. The `UnicodeJSONRenderer` class no longer exists, and the `UNICODE_JSON` setting has been added. To revert this behavior use the new setting:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'UNICODE_JSON': False
|
||||
}
|
||||
|
||||
#### Compact JSON by default.
|
||||
|
||||
We now output compact JSON in responses by default. For example, we return:
|
||||
|
||||
{"email":"amy@example.com","is_admin":true}
|
||||
|
||||
Instead of the following:
|
||||
|
||||
{"email": "amy@example.com", "is_admin": true}
|
||||
|
||||
The `COMPACT_JSON` setting has been added, and can be used to revert this behavior if needed:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'COMPACT_JSON': False
|
||||
}
|
||||
|
||||
#### File fields as URLs
|
||||
|
||||
The `FileField` and `ImageField` classes are now represented as URLs by default. You should ensure you set Django's [standard `MEDIA_URL` setting](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-MEDIA_URL) appropriately, and ensure your application [serves the uploaded files](https://docs.djangoproject.com/en/dev/howto/static-files/#serving-uploaded-files-in-development).
|
||||
|
||||
You can revert this behavior, and display filenames in the representation by using the `UPLOADED_FILES_USE_URL` settings key:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'UPLOADED_FILES_USE_URL': False
|
||||
}
|
||||
|
||||
You can also modify serializer fields individually, using the `use_url` argument:
|
||||
|
||||
uploaded_file = serializers.FileField(use_url=False)
|
||||
|
||||
Also note that you should pass the `request` object to the serializer as context when instantiating it, so that a fully qualified URL can be returned. Returned URLs will then be of the form `https://example.com/url_path/filename.txt`. For example:
|
||||
|
||||
context = {'request': request}
|
||||
serializer = ExampleSerializer(instance, context=context)
|
||||
return Response(serializer.data)
|
||||
|
||||
If the request is omitted from the context, the returned URLs will be of the form `/url_path/filename.txt`.
|
||||
|
||||
#### Throttle headers using `Retry-After`.
|
||||
|
||||
The custom `X-Throttle-Wait-Second` header has now been dropped in favor of the standard `Retry-After` header. You can revert this behavior if needed by writing a custom exception handler for your application.
|
||||
|
||||
#### Date and time objects as ISO-8859-1 strings in serializer data.
|
||||
|
||||
Date and Time objects are now coerced to strings by default in the serializer output. Previously they were returned as `Date`, `Time` and `DateTime` objects, and later coerced to strings by the renderer.
|
||||
|
||||
You can modify this behavior globally by settings the existing `DATE_FORMAT`, `DATETIME_FORMAT` and `TIME_FORMAT` settings keys. Setting these values to `None` instead of their default value of `'iso-8859-1'` will result in native objects being returned in serializer data.
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
# Return native `Date` and `Time` objects in `serializer.data`
|
||||
'DATETIME_FORMAT': None
|
||||
'DATE_FORMAT': None
|
||||
'TIME_FORMAT': None
|
||||
}
|
||||
|
||||
You can also modify serializer fields individually, using the `date_format`, `time_format` and `datetime_format` arguments:
|
||||
|
||||
# Return `DateTime` instances in `serializer.data`, not strings.
|
||||
created = serializers.DateTimeField(format=None)
|
||||
|
||||
#### Decimals as strings in serializer data.
|
||||
|
||||
Decimals are now coerced to strings by default in the serializer output. Previously they were returned as `Decimal` objects, and later coerced to strings by the renderer.
|
||||
|
||||
You can modify this behavior globally by using the `COERCE_DECIMAL_TO_STRING` settings key.
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'COERCE_DECIMAL_TO_STRING': False
|
||||
}
|
||||
|
||||
Or modify it on an individual serializer field, using the `coerce_to_string` keyword argument.
|
||||
|
||||
# Return `Decimal` instances in `serializer.data`, not strings.
|
||||
amount = serializers.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
coerce_to_string=False
|
||||
)
|
||||
|
||||
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).
|
|
@ -109,7 +109,7 @@ You can also use the excellent [tox][tox] testing tool to run the tests against
|
|||
|
||||
It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission.
|
||||
|
||||
It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another seperate issue without interfering with an ongoing pull requests.
|
||||
It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another separate issue without interfering with an ongoing pull requests.
|
||||
|
||||
It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests.
|
||||
|
||||
|
|
|
@ -159,7 +159,7 @@ You can determine your currently installed version using `pip freeze`:
|
|||
* Added `write_only_fields` option to `ModelSerializer` classes.
|
||||
* JSON renderer now deals with objects that implement a dict-like interface.
|
||||
* Fix compatiblity with newer versions of `django-oauth-plus`.
|
||||
* Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations.
|
||||
* Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unnecessary queryset re-evaluations.
|
||||
* Bugfix: Allow defaults on BooleanFields to be properly honored when values are not supplied.
|
||||
* Bugfix: Prevent double-escaping of non-latin1 URL query params when appending `format=json` params.
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
Although flat data structures serve to properly delineate between the individual entities in your service, there are cases where it may be more appropriate or convenient to use nested data structures.
|
||||
|
||||
Nested data structures are easy enough to work with if they're read-only - simply nest your serializer classes and you're good to go. However, there are a few more subtleties to using writable nested serializers, due to the dependancies between the various model instances, and the need to save or delete multiple instances in a single action.
|
||||
Nested data structures are easy enough to work with if they're read-only - simply nest your serializer classes and you're good to go. However, there are a few more subtleties to using writable nested serializers, due to the dependencies between the various model instances, and the need to save or delete multiple instances in a single action.
|
||||
|
||||
## One-to-many data structures
|
||||
|
||||
|
|
|
@ -41,20 +41,7 @@ Once that's done we can create an app that we'll use to create a simple Web API.
|
|||
|
||||
python manage.py startapp snippets
|
||||
|
||||
The simplest way to get up and running will probably be to use an `sqlite3` database for the tutorial. Edit the `tutorial/settings.py` file, and set the default database `"ENGINE"` to `"sqlite3"`, and `"NAME"` to `"tmp.db"`.
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': 'tmp.db',
|
||||
'USER': '',
|
||||
'PASSWORD': '',
|
||||
'HOST': '',
|
||||
'PORT': '',
|
||||
}
|
||||
}
|
||||
|
||||
We'll also need to add our new `snippets` app and the `rest_framework` app to `INSTALLED_APPS`.
|
||||
We'll need to add our new `snippets` app and the `rest_framework` app to `INSTALLED_APPS`. Let's edit the `tutorial/settings.py` file:
|
||||
|
||||
INSTALLED_APPS = (
|
||||
...
|
||||
|
@ -72,7 +59,7 @@ Okay, we're ready to roll.
|
|||
|
||||
## Creating a model to work with
|
||||
|
||||
For the purposes of this tutorial we're going to start by creating a simple `Snippet` model that is used to store code snippets. Go ahead and edit the `snippets` app's `models.py` file. Note: Good programming practices include comments. Although you will find them in our repository version of this tutorial code, we have omitted them here to focus on the code itself.
|
||||
For the purposes of this tutorial we're going to start by creating a simple `Snippet` model that is used to store code snippets. Go ahead and edit the `snippets/models.py` file. Note: Good programming practices include comments. Although you will find them in our repository version of this tutorial code, we have omitted them here to focus on the code itself.
|
||||
|
||||
from django.db import models
|
||||
from pygments.lexers import get_all_lexers
|
||||
|
@ -98,9 +85,10 @@ For the purposes of this tutorial we're going to start by creating a simple `Sni
|
|||
class Meta:
|
||||
ordering = ('created',)
|
||||
|
||||
Don't forget to sync the database for the first time.
|
||||
We'll also need to create an initial migration for our snippet model, and sync the database for the first time.
|
||||
|
||||
python manage.py syncdb
|
||||
python manage.py makemigrations snippets
|
||||
python manage.py migrate
|
||||
|
||||
## Creating a Serializer class
|
||||
|
||||
|
@ -112,40 +100,39 @@ The first thing we need to get started on our Web API is to provide a way of ser
|
|||
|
||||
|
||||
class SnippetSerializer(serializers.Serializer):
|
||||
pk = serializers.Field() # Note: `Field` is an untyped read-only field.
|
||||
pk = serializers.IntegerField(read_only=True)
|
||||
title = serializers.CharField(required=False,
|
||||
max_length=100)
|
||||
code = serializers.CharField(widget=widgets.Textarea,
|
||||
max_length=100000)
|
||||
code = serializers.CharField(style={'type': 'textarea'})
|
||||
linenos = serializers.BooleanField(required=False)
|
||||
language = serializers.ChoiceField(choices=LANGUAGE_CHOICES,
|
||||
default='python')
|
||||
style = serializers.ChoiceField(choices=STYLE_CHOICES,
|
||||
default='friendly')
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
def create(self, validated_attrs):
|
||||
"""
|
||||
Create or update a new snippet instance, given a dictionary
|
||||
of deserialized field values.
|
||||
|
||||
Note that if we don't define this method, then deserializing
|
||||
data will simply return a dictionary of items.
|
||||
Create and return a new `Snippet` instance, given the validated data.
|
||||
"""
|
||||
if instance:
|
||||
# Update existing instance
|
||||
instance.title = attrs.get('title', instance.title)
|
||||
instance.code = attrs.get('code', instance.code)
|
||||
instance.linenos = attrs.get('linenos', instance.linenos)
|
||||
instance.language = attrs.get('language', instance.language)
|
||||
instance.style = attrs.get('style', instance.style)
|
||||
return instance
|
||||
return Snippet.objects.create(**validated_attrs)
|
||||
|
||||
# Create new instance
|
||||
return Snippet(**attrs)
|
||||
def update(self, instance, validated_attrs):
|
||||
"""
|
||||
Update and return an existing `Snippet` instance, given the validated data.
|
||||
"""
|
||||
instance.title = validated_attrs.get('title', instance.title)
|
||||
instance.code = validated_attrs.get('code', instance.code)
|
||||
instance.linenos = validated_attrs.get('linenos', instance.linenos)
|
||||
instance.language = validated_attrs.get('language', instance.language)
|
||||
instance.style = validated_attrs.get('style', instance.style)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
The first part of the serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
|
||||
The first part of the serializer class defines the fields that get serialized/deserialized. The `create()` and `update()` methods define how fully fledged instances are created or modified when calling `serializer.save()`
|
||||
|
||||
Notice that we can also use various attributes that would typically be used on form fields, such as `widget=widgets.Textarea`. These can be used to control how the serializer should render when displayed as an HTML form. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
|
||||
A serializer class is very similar to a Django `Form` class, and includes similar validation flags on the various fields, such as `required`, `max_length` and `default`.
|
||||
|
||||
The field flags can also control how the serializer should be displayed in certain circumstances, such as when rendering to HTML. The `style={'type': 'textarea'}` flag above is equivelent to using `widget=widgets.Textarea` on a Django `Form` class. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
|
||||
|
||||
We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit.
|
||||
|
||||
|
@ -219,6 +206,24 @@ Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer`
|
|||
model = Snippet
|
||||
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
|
||||
|
||||
Once nice property that serializers have is that you can inspect all the fields an serializer instance, by printing it's representation. Open the Django shell with `python manange.py shell`, then try the following:
|
||||
|
||||
>>> from snippets.serializers import SnippetSerializer
|
||||
>>> serializer = SnippetSerializer()
|
||||
>>> print repr(serializer) # In python 3 use `print(repr(serializer))`
|
||||
SnippetSerializer():
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
title = CharField(allow_blank=True, max_length=100, required=False)
|
||||
code = CharField(style={'type': 'textarea'})
|
||||
linenos = BooleanField(required=False)
|
||||
language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')...
|
||||
style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')...
|
||||
|
||||
It's important to remember that `ModelSerializer` classes don't do anything particularly magically, they are simply a shortcut to creating a serializer class with:
|
||||
|
||||
* An automatically determined set of fields.
|
||||
* Simple default implementations for the `create()` and `update()` methods.
|
||||
|
||||
## Writing regular Django views using our Serializer
|
||||
|
||||
Let's see how we can write some API views using our new Serializer class.
|
||||
|
|
|
@ -92,24 +92,26 @@ Finally we need to add those views into the API, by referencing them from the UR
|
|||
|
||||
Right now, if we created a code snippet, there'd be no way of associating the user that created the snippet, with the snippet instance. The user isn't sent as part of the serialized representation, but is instead a property of the incoming request.
|
||||
|
||||
The way we deal with that is by overriding a `.pre_save()` method on our snippet views, that allows us to handle any information that is implicit in the incoming request or requested URL.
|
||||
The way we deal with that is by overriding a `.perform_create()` method on our snippet views, that allows us to modify how the instance save is managed, and handle any information that is implicit in the incoming request or requested URL.
|
||||
|
||||
On **both** the `SnippetList` and `SnippetDetail` view classes, add the following method:
|
||||
On the `SnippetList` view class, add the following method:
|
||||
|
||||
def pre_save(self, obj):
|
||||
obj.owner = self.request.user
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(owner=self.request.user)
|
||||
|
||||
The `create()` method of our serializer will now be passed an additional `'owner'` field, along with the validated data from the request.
|
||||
|
||||
## Updating our serializer
|
||||
|
||||
Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition in `serializers.py`:
|
||||
|
||||
owner = serializers.Field(source='owner.username')
|
||||
owner = serializers.ReadOnlyField(source='owner.username')
|
||||
|
||||
**Note**: Make sure you also add `'owner',` to the list of fields in the inner `Meta` class.
|
||||
|
||||
This field is doing something quite interesting. The `source` argument controls which attribute is used to populate a field, and can point at any attribute on the serialized instance. It can also take the dotted notation shown above, in which case it will traverse the given attributes, in a similar way as it is used with Django's template language.
|
||||
|
||||
The field we've added is the untyped `Field` class, in contrast to the other typed fields, such as `CharField`, `BooleanField` etc... The untyped `Field` is always read-only, and will be used for serialized representations, but will not be used for updating model instances when they are deserialized.
|
||||
The field we've added is the untyped `ReadOnlyField` class, in contrast to the other typed fields, such as `CharField`, `BooleanField` etc... The untyped `ReadOnlyField` is always read-only, and will be used for serialized representations, but will not be used for updating model instances when they are deserialized. We could have also used `CharField(read_only=True)` here.
|
||||
|
||||
## Adding required permissions to views
|
||||
|
||||
|
|
|
@ -26,11 +26,13 @@ Create a new Django project named `tutorial`, then start a new app called `quick
|
|||
|
||||
Now sync your database for the first time:
|
||||
|
||||
python manage.py syncdb
|
||||
python manage.py migrate
|
||||
|
||||
Make sure to create an initial user named `admin` with a password of `password`. We'll authenticate as that user later in our example.
|
||||
We'll also create an initial user named `admin` with a password of `password`. We'll authenticate as that user later in our example.
|
||||
|
||||
Once you've set up a database and got everything synced and ready to go, open up the app's directory and we'll get coding...
|
||||
python manage.py createsuperuser
|
||||
|
||||
Once you've set up a database and initial user created and ready to go, open up the app's directory and we'll get coding...
|
||||
|
||||
## Serializers
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ path_list = [
|
|||
'api-guide/serializers.md',
|
||||
'api-guide/fields.md',
|
||||
'api-guide/relations.md',
|
||||
'api-guide/validators.md',
|
||||
'api-guide/authentication.md',
|
||||
'api-guide/permissions.md',
|
||||
'api-guide/throttling.md',
|
||||
|
|
|
@ -8,8 +8,8 @@ flake8==2.2.2
|
|||
markdown>=2.1.0
|
||||
PyYAML>=3.10
|
||||
defusedxml>=0.3
|
||||
django-guardian==1.2.4
|
||||
django-filter>=0.5.4
|
||||
django-oauth-plus>=2.2.1
|
||||
oauth2>=1.5.211
|
||||
django-oauth2-provider>=0.2.4
|
||||
Pillow==2.3.0
|
||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __
|
|||
"""
|
||||
|
||||
__title__ = 'Django REST framework'
|
||||
__version__ = '2.4.4'
|
||||
__version__ = '3.0.0'
|
||||
__author__ = 'Tom Christie'
|
||||
__license__ = 'BSD 2-Clause'
|
||||
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
||||
|
|
|
@ -129,7 +129,7 @@ class SessionAuthentication(BaseAuthentication):
|
|||
reason = CSRFCheck().process_view(request, None, (), {})
|
||||
if reason:
|
||||
# CSRF failed, bail with explicit error message
|
||||
raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason)
|
||||
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
|
||||
|
||||
|
||||
class TokenAuthentication(BaseAuthentication):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from django.contrib.auth import authenticate
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework import exceptions, serializers
|
||||
|
||||
|
||||
class AuthTokenSerializer(serializers.Serializer):
|
||||
|
@ -18,12 +18,13 @@ class AuthTokenSerializer(serializers.Serializer):
|
|||
if user:
|
||||
if not user.is_active:
|
||||
msg = _('User account is disabled.')
|
||||
raise serializers.ValidationError(msg)
|
||||
attrs['user'] = user
|
||||
return attrs
|
||||
raise exceptions.ValidationError(msg)
|
||||
else:
|
||||
msg = _('Unable to log in with provided credentials.')
|
||||
raise serializers.ValidationError(msg)
|
||||
raise exceptions.ValidationError(msg)
|
||||
else:
|
||||
msg = _('Must include "username" and "password"')
|
||||
raise serializers.ValidationError(msg)
|
||||
raise exceptions.ValidationError(msg)
|
||||
|
||||
attrs['user'] = user
|
||||
return attrs
|
||||
|
|
|
@ -16,9 +16,10 @@ 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():
|
||||
token, created = Token.objects.get_or_create(user=serializer.object['user'])
|
||||
user = serializer.validated_data['user']
|
||||
token, created = Token.objects.get_or_create(user=user)
|
||||
return Response({'token': token.key})
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
|
|
@ -5,11 +5,12 @@ versions of django/python, and compatibility wrappers around optional packages.
|
|||
|
||||
# flake8: noqa
|
||||
from __future__ import unicode_literals
|
||||
import django
|
||||
import inspect
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.conf import settings
|
||||
from django.utils import six
|
||||
import django
|
||||
import inspect
|
||||
|
||||
|
||||
# Handle django.utils.encoding rename in 1.5 onwards.
|
||||
|
@ -39,6 +40,17 @@ except ImportError:
|
|||
django_filters = None
|
||||
|
||||
|
||||
if django.VERSION >= (1, 6):
|
||||
def clean_manytomany_helptext(text):
|
||||
return text
|
||||
else:
|
||||
# Up to version 1.5 many to many fields automatically suffix
|
||||
# the `help_text` attribute with hardcoded text.
|
||||
def clean_manytomany_helptext(text):
|
||||
if text.endswith(' Hold down "Control", or "Command" on a Mac, to select more than one.'):
|
||||
text = text[:-69]
|
||||
return text
|
||||
|
||||
# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
|
||||
# Fixes (#1712). We keep the try/except for the test suite.
|
||||
guardian = None
|
||||
|
@ -73,15 +85,6 @@ except ImportError:
|
|||
from collections import UserDict
|
||||
from collections import MutableMapping as DictMixin
|
||||
|
||||
# Try to import PIL in either of the two ways it can end up installed.
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
try:
|
||||
import Image
|
||||
except ImportError:
|
||||
Image = None
|
||||
|
||||
|
||||
def get_model_name(model_cls):
|
||||
try:
|
||||
|
@ -110,6 +113,62 @@ else:
|
|||
return [m.upper() for m in self.http_method_names if hasattr(self, m)]
|
||||
|
||||
|
||||
|
||||
# MinValueValidator, MaxValueValidator et al. only accept `message` in 1.8+
|
||||
if django.VERSION >= (1, 8):
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.core.validators import MinLengthValidator, MaxLengthValidator
|
||||
else:
|
||||
from django.core.validators import MinValueValidator as DjangoMinValueValidator
|
||||
from django.core.validators import MaxValueValidator as DjangoMaxValueValidator
|
||||
from django.core.validators import MinLengthValidator as DjangoMinLengthValidator
|
||||
from django.core.validators import MaxLengthValidator as DjangoMaxLengthValidator
|
||||
|
||||
class MinValueValidator(DjangoMinValueValidator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.message = kwargs.pop('message', self.message)
|
||||
super(MinValueValidator, self).__init__(*args, **kwargs)
|
||||
|
||||
class MaxValueValidator(DjangoMaxValueValidator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.message = kwargs.pop('message', self.message)
|
||||
super(MaxValueValidator, self).__init__(*args, **kwargs)
|
||||
|
||||
class MinLengthValidator(DjangoMinLengthValidator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.message = kwargs.pop('message', self.message)
|
||||
super(MinLengthValidator, self).__init__(*args, **kwargs)
|
||||
|
||||
class MaxLengthValidator(DjangoMaxLengthValidator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.message = kwargs.pop('message', self.message)
|
||||
super(MaxLengthValidator, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
# URLValidator only accepts `message` in 1.6+
|
||||
if django.VERSION >= (1, 6):
|
||||
from django.core.validators import URLValidator
|
||||
else:
|
||||
from django.core.validators import URLValidator as DjangoURLValidator
|
||||
|
||||
class URLValidator(DjangoURLValidator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.message = kwargs.pop('message', self.message)
|
||||
super(URLValidator, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
# EmailValidator requires explicit regex prior to 1.6+
|
||||
if django.VERSION >= (1, 6):
|
||||
from django.core.validators import EmailValidator
|
||||
else:
|
||||
from django.core.validators import EmailValidator as DjangoEmailValidator
|
||||
from django.core.validators import email_re
|
||||
|
||||
class EmailValidator(DjangoEmailValidator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EmailValidator, self).__init__(email_re, *args, **kwargs)
|
||||
|
||||
|
||||
# PATCH method is not implemented by Django
|
||||
if 'patch' not in View.http_method_names:
|
||||
View.http_method_names = View.http_method_names + ['patch']
|
||||
|
@ -133,12 +192,12 @@ class RequestFactory(DjangoRequestFactory):
|
|||
r = {
|
||||
'PATH_INFO': self._get_path(parsed),
|
||||
'QUERY_STRING': force_text(parsed[4]),
|
||||
'REQUEST_METHOD': str(method),
|
||||
'REQUEST_METHOD': six.text_type(method),
|
||||
}
|
||||
if data:
|
||||
r.update({
|
||||
'CONTENT_LENGTH': len(data),
|
||||
'CONTENT_TYPE': str(content_type),
|
||||
'CONTENT_TYPE': six.text_type(content_type),
|
||||
'wsgi.input': FakePayload(data),
|
||||
})
|
||||
elif django.VERSION <= (1, 4):
|
||||
|
@ -232,6 +291,15 @@ except ImportError:
|
|||
oauth2_constants = None
|
||||
provider_now = None
|
||||
|
||||
# `seperators` argument to `json.dumps()` differs between 2.x and 3.x
|
||||
# See: http://bugs.python.org/issue22767
|
||||
if six.PY3:
|
||||
SHORT_SEPARATORS = (',', ':')
|
||||
LONG_SEPARATORS = (', ', ': ')
|
||||
else:
|
||||
SHORT_SEPARATORS = (b',', b':')
|
||||
LONG_SEPARATORS = (b', ', b': ')
|
||||
|
||||
|
||||
# Handle lazy strings across Py2/Py3
|
||||
from django.utils.functional import Promise
|
||||
|
|
|
@ -10,7 +10,6 @@ from __future__ import unicode_literals
|
|||
from django.utils import six
|
||||
from rest_framework.views import APIView
|
||||
import types
|
||||
import warnings
|
||||
|
||||
|
||||
def api_view(http_method_names):
|
||||
|
@ -130,37 +129,3 @@ def list_route(methods=['get'], **kwargs):
|
|||
func.kwargs = kwargs
|
||||
return func
|
||||
return decorator
|
||||
|
||||
|
||||
# These are now pending deprecation, in favor of `detail_route` and `list_route`.
|
||||
|
||||
def link(**kwargs):
|
||||
"""
|
||||
Used to mark a method on a ViewSet that should be routed for detail GET requests.
|
||||
"""
|
||||
msg = 'link is pending deprecation. Use detail_route instead.'
|
||||
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||
|
||||
def decorator(func):
|
||||
func.bind_to_methods = ['get']
|
||||
func.detail = True
|
||||
func.kwargs = kwargs
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def action(methods=['post'], **kwargs):
|
||||
"""
|
||||
Used to mark a method on a ViewSet that should be routed for detail POST requests.
|
||||
"""
|
||||
msg = 'action is pending deprecation. Use detail_route instead.'
|
||||
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||
|
||||
def decorator(func):
|
||||
func.bind_to_methods = methods
|
||||
func.detail = True
|
||||
func.kwargs = kwargs
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
|
|
@ -15,7 +15,7 @@ class APIException(Exception):
|
|||
Subclasses should provide `.status_code` and `.default_detail` properties.
|
||||
"""
|
||||
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
default_detail = ''
|
||||
default_detail = 'A server error occured'
|
||||
|
||||
def __init__(self, detail=None):
|
||||
self.detail = detail or self.default_detail
|
||||
|
@ -24,6 +24,27 @@ class APIException(Exception):
|
|||
return self.detail
|
||||
|
||||
|
||||
# The recommended style for using `ValidationError` is to keep it namespaced
|
||||
# under `serializers`, in order to minimize potential confusion with Django's
|
||||
# built in `ValidationError`. For example:
|
||||
#
|
||||
# from rest_framework import serializers
|
||||
# raise serializers.ValidationError('Value was invalid')
|
||||
|
||||
class ValidationError(APIException):
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def __init__(self, detail):
|
||||
# For validation errors the 'detail' key is always required.
|
||||
# The details should always be coerced to a list if not already.
|
||||
if not isinstance(detail, dict) and not isinstance(detail, list):
|
||||
detail = [detail]
|
||||
self.detail = detail
|
||||
|
||||
def __str__(self):
|
||||
return str(self.detail)
|
||||
|
||||
|
||||
class ParseError(APIException):
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
default_detail = 'Malformed request.'
|
||||
|
@ -54,7 +75,7 @@ class MethodNotAllowed(APIException):
|
|||
|
||||
class NotAcceptable(APIException):
|
||||
status_code = status.HTTP_406_NOT_ACCEPTABLE
|
||||
default_detail = "Could not satisfy the request's Accept header"
|
||||
default_detail = "Could not satisfy the request Accept header"
|
||||
|
||||
def __init__(self, detail=None, available_renderers=None):
|
||||
self.detail = detail or self.default_detail
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,6 +3,7 @@ Provides generic filtering backends that can be used to filter the results
|
|||
returned by list views.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
from django.utils import six
|
||||
|
@ -64,7 +65,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 +79,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):
|
||||
|
@ -97,7 +98,7 @@ class SearchFilter(BaseFilterBackend):
|
|||
if not search_fields:
|
||||
return queryset
|
||||
|
||||
orm_lookups = [self.construct_search(str(search_field))
|
||||
orm_lookups = [self.construct_search(six.text_type(search_field))
|
||||
for search_field in search_fields]
|
||||
|
||||
for search_term in self.get_search_terms(request):
|
||||
|
@ -121,7 +122,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(',')]
|
||||
|
||||
|
@ -147,7 +148,7 @@ class OrderingFilter(BaseFilterBackend):
|
|||
if not getattr(field, 'write_only', False)
|
||||
]
|
||||
elif valid_fields == '__all__':
|
||||
# View explictly allows filtering on any model field
|
||||
# View explicitly allows filtering on any model field
|
||||
valid_fields = [field.name for field in queryset.model._meta.fields]
|
||||
valid_fields += queryset.query.aggregates.keys()
|
||||
|
||||
|
|
|
@ -3,15 +3,14 @@ Generic views that provide commonly needed behaviour.
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
||||
from django.core.paginator import Paginator, InvalidPage
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404 as _get_object_or_404
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import views, mixins, exceptions
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework import views, mixins
|
||||
from rest_framework.settings import api_settings
|
||||
import warnings
|
||||
|
||||
|
||||
def strict_positive_int(integer_string, cutoff=None):
|
||||
|
@ -28,7 +27,7 @@ def strict_positive_int(integer_string, cutoff=None):
|
|||
|
||||
def get_object_or_404(queryset, *filter_args, **filter_kwargs):
|
||||
"""
|
||||
Same as Django's standard shortcut, but make sure to raise 404
|
||||
Same as Django's standard shortcut, but make sure to also raise 404
|
||||
if the filter_kwargs don't match the required types.
|
||||
"""
|
||||
try:
|
||||
|
@ -51,11 +50,6 @@ class GenericAPIView(views.APIView):
|
|||
queryset = None
|
||||
serializer_class = None
|
||||
|
||||
# This shortcut may be used instead of setting either or both
|
||||
# of the `queryset`/`serializer_class` attributes, although using
|
||||
# the explicit style is generally preferred.
|
||||
model = None
|
||||
|
||||
# If you want to use object lookups other than pk, set this attribute.
|
||||
# For more complex lookup requirements override `get_object()`.
|
||||
lookup_field = 'pk'
|
||||
|
@ -71,20 +65,10 @@ class GenericAPIView(views.APIView):
|
|||
# The filter backend classes to use for queryset filtering
|
||||
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
|
||||
|
||||
# The following attributes may be subject to change,
|
||||
# The following attribute may be subject to change,
|
||||
# and should be considered private API.
|
||||
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
|
||||
paginator_class = Paginator
|
||||
|
||||
######################################
|
||||
# These are pending deprecation...
|
||||
|
||||
pk_url_kwarg = 'pk'
|
||||
slug_url_kwarg = 'slug'
|
||||
slug_field = 'slug'
|
||||
allow_empty = True
|
||||
filter_backend = api_settings.FILTER_BACKEND
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""
|
||||
Extra context provided to the serializer class.
|
||||
|
@ -95,18 +79,16 @@ class GenericAPIView(views.APIView):
|
|||
'view': self
|
||||
}
|
||||
|
||||
def get_serializer(self, instance=None, data=None, files=None, many=False,
|
||||
partial=False, allow_add_remove=False):
|
||||
def get_serializer(self, instance=None, data=None, many=False, partial=False):
|
||||
"""
|
||||
Return the serializer instance that should be used for validating and
|
||||
deserializing input, and for serializing output.
|
||||
"""
|
||||
serializer_class = self.get_serializer_class()
|
||||
context = self.get_serializer_context()
|
||||
return serializer_class(instance, data=data, files=files,
|
||||
many=many, partial=partial,
|
||||
allow_add_remove=allow_add_remove,
|
||||
context=context)
|
||||
return serializer_class(
|
||||
instance, data=data, many=many, partial=partial, context=context
|
||||
)
|
||||
|
||||
def get_pagination_serializer(self, page):
|
||||
"""
|
||||
|
@ -120,39 +102,18 @@ class GenericAPIView(views.APIView):
|
|||
context = self.get_serializer_context()
|
||||
return pagination_serializer_class(instance=page, context=context)
|
||||
|
||||
def paginate_queryset(self, queryset, page_size=None):
|
||||
def paginate_queryset(self, queryset):
|
||||
"""
|
||||
Paginate a queryset if required, either returning a page object,
|
||||
or `None` if pagination is not configured for this view.
|
||||
"""
|
||||
deprecated_style = False
|
||||
if page_size is not None:
|
||||
warnings.warn('The `page_size` parameter to `paginate_queryset()` '
|
||||
'is deprecated. '
|
||||
'Note that the return style of this method is also '
|
||||
'changed, and will simply return a page object '
|
||||
'when called without a `page_size` argument.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
deprecated_style = True
|
||||
else:
|
||||
# Determine the required page size.
|
||||
# If pagination is not configured, simply return None.
|
||||
page_size = self.get_paginate_by()
|
||||
if not page_size:
|
||||
return None
|
||||
page_size = self.get_paginate_by()
|
||||
if not page_size:
|
||||
return None
|
||||
|
||||
if not self.allow_empty:
|
||||
warnings.warn(
|
||||
'The `allow_empty` parameter is deprecated. '
|
||||
'To use `allow_empty=False` style behavior, You should override '
|
||||
'`get_queryset()` and explicitly raise a 404 on empty querysets.',
|
||||
DeprecationWarning, stacklevel=2
|
||||
)
|
||||
|
||||
paginator = self.paginator_class(queryset, page_size,
|
||||
allow_empty_first_page=self.allow_empty)
|
||||
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)
|
||||
|
@ -167,11 +128,9 @@ class GenericAPIView(views.APIView):
|
|||
error_format = _('Invalid page (%(page_number)s): %(message)s')
|
||||
raise Http404(error_format % {
|
||||
'page_number': page_number,
|
||||
'message': str(exc)
|
||||
'message': six.text_type(exc)
|
||||
})
|
||||
|
||||
if deprecated_style:
|
||||
return (paginator, page, page.object_list, page.has_other_pages())
|
||||
return page
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
|
@ -191,29 +150,12 @@ class GenericAPIView(views.APIView):
|
|||
"""
|
||||
Returns the list of filter backends that this view requires.
|
||||
"""
|
||||
if self.filter_backends is None:
|
||||
filter_backends = []
|
||||
else:
|
||||
# Note that we are returning a *copy* of the class attribute,
|
||||
# so that it is safe for the view to mutate it if needed.
|
||||
filter_backends = list(self.filter_backends)
|
||||
|
||||
if not filter_backends and self.filter_backend:
|
||||
warnings.warn(
|
||||
'The `filter_backend` attribute and `FILTER_BACKEND` setting '
|
||||
'are deprecated in favor of a `filter_backends` '
|
||||
'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take '
|
||||
'a *list* of filter backend classes.',
|
||||
DeprecationWarning, stacklevel=2
|
||||
)
|
||||
filter_backends = [self.filter_backend]
|
||||
|
||||
return filter_backends
|
||||
return list(self.filter_backends)
|
||||
|
||||
# The following methods provide default implementations
|
||||
# that you may want to override for more complex cases.
|
||||
|
||||
def get_paginate_by(self, queryset=None):
|
||||
def get_paginate_by(self):
|
||||
"""
|
||||
Return the size of pages to use with pagination.
|
||||
|
||||
|
@ -222,15 +164,10 @@ class GenericAPIView(views.APIView):
|
|||
|
||||
Otherwise defaults to using `self.paginate_by`.
|
||||
"""
|
||||
if queryset is not None:
|
||||
warnings.warn('The `queryset` parameter to `get_paginate_by()` '
|
||||
'is deprecated.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
|
||||
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):
|
||||
|
@ -248,26 +185,13 @@ class GenericAPIView(views.APIView):
|
|||
|
||||
(Eg. admins get full serialization, others get basic serialization)
|
||||
"""
|
||||
serializer_class = self.serializer_class
|
||||
if serializer_class is not None:
|
||||
return serializer_class
|
||||
|
||||
warnings.warn(
|
||||
'The `.model` attribute on view classes is now deprecated in favor '
|
||||
'of the more explicit `serializer_class` and `queryset` attributes.',
|
||||
DeprecationWarning, stacklevel=2
|
||||
assert self.serializer_class is not None, (
|
||||
"'%s' should either include a `serializer_class` attribute, "
|
||||
"or override the `get_serializer_class()` method."
|
||||
% self.__class__.__name__
|
||||
)
|
||||
|
||||
assert self.model is not None, \
|
||||
"'%s' should either include a 'serializer_class' attribute, " \
|
||||
"or use the 'model' attribute as a shortcut for " \
|
||||
"automatically generating a serializer class." \
|
||||
% self.__class__.__name__
|
||||
|
||||
class DefaultSerializer(self.model_serializer_class):
|
||||
class Meta:
|
||||
model = self.model
|
||||
return DefaultSerializer
|
||||
return self.serializer_class
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
|
@ -284,21 +208,19 @@ class GenericAPIView(views.APIView):
|
|||
|
||||
(Eg. return a list of items that is specific to the user)
|
||||
"""
|
||||
if self.queryset is not None:
|
||||
return self.queryset._clone()
|
||||
assert self.queryset is not None, (
|
||||
"'%s' should either include a `queryset` attribute, "
|
||||
"or override the `get_queryset()` method."
|
||||
% self.__class__.__name__
|
||||
)
|
||||
|
||||
if self.model is not None:
|
||||
warnings.warn(
|
||||
'The `.model` attribute on view classes is now deprecated in favor '
|
||||
'of the more explicit `serializer_class` and `queryset` attributes.',
|
||||
DeprecationWarning, stacklevel=2
|
||||
)
|
||||
return self.model._default_manager.all()
|
||||
queryset = self.queryset
|
||||
if isinstance(queryset, QuerySet):
|
||||
# Ensure queryset is re-evaluated on each request.
|
||||
queryset = queryset.all()
|
||||
return queryset
|
||||
|
||||
error_format = "'%s' must define 'queryset' or 'model'"
|
||||
raise ImproperlyConfigured(error_format % self.__class__.__name__)
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
def get_object(self):
|
||||
"""
|
||||
Returns the object the view is displaying.
|
||||
|
||||
|
@ -306,43 +228,19 @@ class GenericAPIView(views.APIView):
|
|||
queryset lookups. Eg if objects are referenced using multiple
|
||||
keyword arguments in the url conf.
|
||||
"""
|
||||
# Determine the base queryset to use.
|
||||
if queryset is None:
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
else:
|
||||
pass # Deprecation warning
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
# Perform the lookup filtering.
|
||||
# Note that `pk` and `slug` are deprecated styles of lookup filtering.
|
||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||
lookup = self.kwargs.get(lookup_url_kwarg, None)
|
||||
pk = self.kwargs.get(self.pk_url_kwarg, None)
|
||||
slug = self.kwargs.get(self.slug_url_kwarg, None)
|
||||
|
||||
if lookup is not None:
|
||||
filter_kwargs = {self.lookup_field: lookup}
|
||||
elif pk is not None and self.lookup_field == 'pk':
|
||||
warnings.warn(
|
||||
'The `pk_url_kwarg` attribute is deprecated. '
|
||||
'Use the `lookup_field` attribute instead',
|
||||
DeprecationWarning
|
||||
)
|
||||
filter_kwargs = {'pk': pk}
|
||||
elif slug is not None and self.lookup_field == 'pk':
|
||||
warnings.warn(
|
||||
'The `slug_url_kwarg` attribute is deprecated. '
|
||||
'Use the `lookup_field` attribute instead',
|
||||
DeprecationWarning
|
||||
)
|
||||
filter_kwargs = {self.slug_field: slug}
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
'Expected view %s to be called with a URL keyword argument '
|
||||
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
||||
'attribute on the view correctly.' %
|
||||
(self.__class__.__name__, self.lookup_field)
|
||||
)
|
||||
assert lookup_url_kwarg in self.kwargs, (
|
||||
'Expected view %s to be called with a URL keyword argument '
|
||||
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
||||
'attribute on the view correctly.' %
|
||||
(self.__class__.__name__, lookup_url_kwarg)
|
||||
)
|
||||
|
||||
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
|
||||
obj = get_object_or_404(queryset, **filter_kwargs)
|
||||
|
||||
# May raise a permission denied
|
||||
|
@ -350,84 +248,6 @@ class GenericAPIView(views.APIView):
|
|||
|
||||
return obj
|
||||
|
||||
# The following are placeholder methods,
|
||||
# and are intended to be overridden.
|
||||
#
|
||||
# The are not called by GenericAPIView directly,
|
||||
# but are used by the mixin methods.
|
||||
|
||||
def pre_save(self, obj):
|
||||
"""
|
||||
Placeholder method for calling before saving an object.
|
||||
|
||||
May be used to set attributes on the object that are implicit
|
||||
in either the request, or the url.
|
||||
"""
|
||||
pass
|
||||
|
||||
def post_save(self, obj, created=False):
|
||||
"""
|
||||
Placeholder method for calling after saving an object.
|
||||
"""
|
||||
pass
|
||||
|
||||
def pre_delete(self, obj):
|
||||
"""
|
||||
Placeholder method for calling before deleting an object.
|
||||
"""
|
||||
pass
|
||||
|
||||
def post_delete(self, obj):
|
||||
"""
|
||||
Placeholder method for calling after deleting an object.
|
||||
"""
|
||||
pass
|
||||
|
||||
def metadata(self, request):
|
||||
"""
|
||||
Return a dictionary of metadata about the view.
|
||||
Used to return responses for OPTIONS requests.
|
||||
|
||||
We override the default behavior, and add some extra information
|
||||
about the required request body for POST and PUT operations.
|
||||
"""
|
||||
ret = super(GenericAPIView, self).metadata(request)
|
||||
|
||||
actions = {}
|
||||
for method in ('PUT', 'POST'):
|
||||
if method not in self.allowed_methods:
|
||||
continue
|
||||
|
||||
original_request = self.request
|
||||
self.request = clone_request(request, method)
|
||||
try:
|
||||
# Test global permissions
|
||||
self.check_permissions(self.request)
|
||||
# Test object permissions
|
||||
if method == 'PUT':
|
||||
try:
|
||||
self.get_object()
|
||||
except Http404:
|
||||
# Http404 should be acceptable and the serializer
|
||||
# metadata should be populated. Except this so the
|
||||
# outer "else" clause of the try-except-else block
|
||||
# will be executed.
|
||||
pass
|
||||
except (exceptions.APIException, PermissionDenied):
|
||||
pass
|
||||
else:
|
||||
# If user has appropriate permissions for the view, include
|
||||
# appropriate metadata about the fields that should be supplied.
|
||||
serializer = self.get_serializer()
|
||||
actions[method] = serializer.metadata()
|
||||
finally:
|
||||
self.request = original_request
|
||||
|
||||
if actions:
|
||||
ret['actions'] = actions
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# Concrete view classes that provide method handlers
|
||||
# by composing the mixin classes with the base view.
|
||||
|
@ -543,25 +363,3 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
|
|||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
return self.destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
# Deprecated classes
|
||||
|
||||
class MultipleObjectAPIView(GenericAPIView):
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
'Subclassing `MultipleObjectAPIView` is deprecated. '
|
||||
'You should simply subclass `GenericAPIView` instead.',
|
||||
DeprecationWarning, stacklevel=2
|
||||
)
|
||||
super(MultipleObjectAPIView, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class SingleObjectAPIView(GenericAPIView):
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
'Subclassing `SingleObjectAPIView` is deprecated. '
|
||||
'You should simply subclass `GenericAPIView` instead.',
|
||||
DeprecationWarning, stacklevel=2
|
||||
)
|
||||
super(SingleObjectAPIView, self).__init__(*args, **kwargs)
|
||||
|
|
125
rest_framework/metadata.py
Normal file
125
rest_framework/metadata.py
Normal file
|
@ -0,0 +1,125 @@
|
|||
"""
|
||||
The metadata API is used to allow cusomization of how `OPTIONS` requests
|
||||
are handled. We currently provide a single default implementation that returns
|
||||
some fairly ad-hoc information about the view.
|
||||
|
||||
Future implementations might use JSON schema or other definations in order
|
||||
to return this information in a more standardized way.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from django.utils.datastructures import SortedDict
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework.compat import force_text
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.utils.field_mapping import ClassLookupDict
|
||||
|
||||
|
||||
class BaseMetadata(object):
|
||||
def determine_metadata(self, request, view):
|
||||
"""
|
||||
Return a dictionary of metadata about the view.
|
||||
Used to return responses for OPTIONS requests.
|
||||
"""
|
||||
raise NotImplementedError(".determine_metadata() must be overridden.")
|
||||
|
||||
|
||||
class SimpleMetadata(BaseMetadata):
|
||||
"""
|
||||
This is the default metadata implementation.
|
||||
It returns an ad-hoc set of information about the view.
|
||||
There are not any formalized standards for `OPTIONS` responses
|
||||
for us to base this on.
|
||||
"""
|
||||
label_lookup = ClassLookupDict({
|
||||
serializers.Field: 'field',
|
||||
serializers.BooleanField: 'boolean',
|
||||
serializers.CharField: 'string',
|
||||
serializers.URLField: 'url',
|
||||
serializers.EmailField: 'email',
|
||||
serializers.RegexField: 'regex',
|
||||
serializers.SlugField: 'slug',
|
||||
serializers.IntegerField: 'integer',
|
||||
serializers.FloatField: 'float',
|
||||
serializers.DecimalField: 'decimal',
|
||||
serializers.DateField: 'date',
|
||||
serializers.DateTimeField: 'datetime',
|
||||
serializers.TimeField: 'time',
|
||||
serializers.ChoiceField: 'choice',
|
||||
serializers.MultipleChoiceField: 'multiple choice',
|
||||
serializers.FileField: 'file upload',
|
||||
serializers.ImageField: 'image upload',
|
||||
})
|
||||
|
||||
def determine_metadata(self, request, view):
|
||||
metadata = SortedDict()
|
||||
metadata['name'] = view.get_view_name()
|
||||
metadata['description'] = view.get_view_description()
|
||||
metadata['renders'] = [renderer.media_type for renderer in view.renderer_classes]
|
||||
metadata['parses'] = [parser.media_type for parser in view.parser_classes]
|
||||
if hasattr(view, 'get_serializer'):
|
||||
actions = self.determine_actions(request, view)
|
||||
if actions:
|
||||
metadata['actions'] = actions
|
||||
return metadata
|
||||
|
||||
def determine_actions(self, request, view):
|
||||
"""
|
||||
For generic class based views we return information about
|
||||
the fields that are accepted for 'PUT' and 'POST' methods.
|
||||
"""
|
||||
actions = {}
|
||||
for method in set(['PUT', 'POST']) & set(view.allowed_methods):
|
||||
view.request = clone_request(request, method)
|
||||
try:
|
||||
# Test global permissions
|
||||
if hasattr(view, 'check_permissions'):
|
||||
view.check_permissions(view.request)
|
||||
# Test object permissions
|
||||
if method == 'PUT' and hasattr(view, 'get_object'):
|
||||
view.get_object()
|
||||
except (exceptions.APIException, PermissionDenied, Http404):
|
||||
pass
|
||||
else:
|
||||
# If user has appropriate permissions for the view, include
|
||||
# appropriate metadata about the fields that should be supplied.
|
||||
serializer = view.get_serializer()
|
||||
actions[method] = self.get_serializer_info(serializer)
|
||||
finally:
|
||||
view.request = request
|
||||
|
||||
return actions
|
||||
|
||||
def get_serializer_info(self, serializer):
|
||||
"""
|
||||
Given an instance of a serializer, return a dictionary of metadata
|
||||
about its fields.
|
||||
"""
|
||||
return SortedDict([
|
||||
(field_name, self.get_field_info(field))
|
||||
for field_name, field in serializer.fields.items()
|
||||
])
|
||||
|
||||
def get_field_info(self, field):
|
||||
"""
|
||||
Given an instance of a serializer field, return a dictionary
|
||||
of metadata about it.
|
||||
"""
|
||||
field_info = SortedDict()
|
||||
field_info['type'] = self.label_lookup[field]
|
||||
field_info['required'] = getattr(field, 'required', False)
|
||||
|
||||
for attr in ['read_only', 'label', 'help_text', 'min_length', 'max_length']:
|
||||
value = getattr(field, attr, None)
|
||||
if value is not None and value != '':
|
||||
field_info[attr] = force_text(value, strings_only=True)
|
||||
|
||||
if hasattr(field, 'choices'):
|
||||
field_info['choices'] = [
|
||||
{'value': choice_value, 'display_name': choice_name}
|
||||
for choice_value, choice_name in field.choices.items()
|
||||
]
|
||||
|
||||
return field_info
|
|
@ -6,40 +6,9 @@ which allows mixin classes to be composed in interesting ways.
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.http import Http404
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.settings import api_settings
|
||||
import warnings
|
||||
|
||||
|
||||
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
|
||||
"""
|
||||
Given a model instance, and an optional pk and slug field,
|
||||
return the full list of all other field names on that model.
|
||||
|
||||
For use when performing full_clean on a model instance,
|
||||
so we only clean the required fields.
|
||||
"""
|
||||
include = []
|
||||
|
||||
if pk:
|
||||
# Deprecated
|
||||
pk_field = obj._meta.pk
|
||||
while pk_field.rel:
|
||||
pk_field = pk_field.rel.to._meta.pk
|
||||
include.append(pk_field.name)
|
||||
|
||||
if slug_field:
|
||||
# Deprecated
|
||||
include.append(slug_field)
|
||||
|
||||
if lookup_field and lookup_field != 'pk':
|
||||
include.append(lookup_field)
|
||||
|
||||
return [field.name for field in obj._meta.fields if field.name not in include]
|
||||
|
||||
|
||||
class CreateModelMixin(object):
|
||||
|
@ -47,17 +16,14 @@ class CreateModelMixin(object):
|
|||
Create a model instance.
|
||||
"""
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
if serializer.is_valid():
|
||||
self.pre_save(serializer.object)
|
||||
self.object = serializer.save(force_insert=True)
|
||||
self.post_save(self.object, created=True)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED,
|
||||
headers=headers)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
|
||||
def get_success_headers(self, data):
|
||||
try:
|
||||
|
@ -70,31 +36,13 @@ class ListModelMixin(object):
|
|||
"""
|
||||
List a queryset.
|
||||
"""
|
||||
empty_error = "Empty list and '%(class_name)s.allow_empty' is False."
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
self.object_list = self.filter_queryset(self.get_queryset())
|
||||
|
||||
# Default is to allow empty querysets. This can be altered by setting
|
||||
# `.allow_empty = False`, to raise 404 errors on empty querysets.
|
||||
if not self.allow_empty and not self.object_list:
|
||||
warnings.warn(
|
||||
'The `allow_empty` parameter is deprecated. '
|
||||
'To use `allow_empty=False` style behavior, You should override '
|
||||
'`get_queryset()` and explicitly raise a 404 on empty querysets.',
|
||||
DeprecationWarning
|
||||
)
|
||||
class_name = self.__class__.__name__
|
||||
error_msg = self.empty_error % {'class_name': class_name}
|
||||
raise Http404(error_msg)
|
||||
|
||||
# Switch between paginated or standard style responses
|
||||
page = self.paginate_queryset(self.object_list)
|
||||
instance = self.filter_queryset(self.get_queryset())
|
||||
page = self.paginate_queryset(instance)
|
||||
if page is not None:
|
||||
serializer = self.get_pagination_serializer(page)
|
||||
else:
|
||||
serializer = self.get_serializer(self.object_list, many=True)
|
||||
|
||||
serializer = self.get_serializer(instance, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
|
@ -103,8 +51,8 @@ class RetrieveModelMixin(object):
|
|||
Retrieve a model instance.
|
||||
"""
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
serializer = self.get_serializer(self.object)
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
|
@ -114,83 +62,28 @@ class UpdateModelMixin(object):
|
|||
"""
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
self.object = self.get_object_or_none()
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
return Response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(self.object, data=request.DATA,
|
||||
files=request.FILES, partial=partial)
|
||||
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
self.pre_save(serializer.object)
|
||||
except ValidationError as err:
|
||||
# full_clean on model instance may be called in pre_save,
|
||||
# so we have to handle eventual errors.
|
||||
return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if self.object is None:
|
||||
self.object = serializer.save(force_insert=True)
|
||||
self.post_save(self.object, created=True)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
self.object = serializer.save(force_update=True)
|
||||
self.post_save(self.object, created=False)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
def perform_update(self, serializer):
|
||||
serializer.save()
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
kwargs['partial'] = True
|
||||
return self.update(request, *args, **kwargs)
|
||||
|
||||
def get_object_or_none(self):
|
||||
try:
|
||||
return self.get_object()
|
||||
except Http404:
|
||||
if self.request.method == 'PUT':
|
||||
# For PUT-as-create operation, we need to ensure that we have
|
||||
# relevant permissions, as if this was a POST request. This
|
||||
# will either raise a PermissionDenied exception, or simply
|
||||
# return None.
|
||||
self.check_permissions(clone_request(self.request, 'POST'))
|
||||
else:
|
||||
# PATCH requests where the object does not exist should still
|
||||
# return a 404 response.
|
||||
raise
|
||||
|
||||
def pre_save(self, obj):
|
||||
"""
|
||||
Set any attributes on the object that are implicit in the request.
|
||||
"""
|
||||
# pk and/or slug attributes are implicit in the URL.
|
||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||
lookup = self.kwargs.get(lookup_url_kwarg, None)
|
||||
pk = self.kwargs.get(self.pk_url_kwarg, None)
|
||||
slug = self.kwargs.get(self.slug_url_kwarg, None)
|
||||
slug_field = slug and self.slug_field or None
|
||||
|
||||
if lookup:
|
||||
setattr(obj, self.lookup_field, lookup)
|
||||
|
||||
if pk:
|
||||
setattr(obj, 'pk', pk)
|
||||
|
||||
if slug:
|
||||
setattr(obj, slug_field, slug)
|
||||
|
||||
# Ensure we clean the attributes so that we don't eg return integer
|
||||
# pk using a string representation, as provided by the url conf kwarg.
|
||||
if hasattr(obj, 'full_clean'):
|
||||
exclude = _get_validation_exclusions(obj, pk, slug_field, self.lookup_field)
|
||||
obj.full_clean(exclude)
|
||||
|
||||
|
||||
class DestroyModelMixin(object):
|
||||
"""
|
||||
Destroy a model instance.
|
||||
"""
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
self.pre_delete(obj)
|
||||
obj.delete()
|
||||
self.post_delete(obj)
|
||||
instance = self.get_object()
|
||||
self.perform_destroy(instance)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.delete()
|
||||
|
|
|
@ -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(',')]
|
||||
|
|
|
@ -13,7 +13,7 @@ class NextPageField(serializers.Field):
|
|||
"""
|
||||
page_field = 'page'
|
||||
|
||||
def to_native(self, value):
|
||||
def to_representation(self, value):
|
||||
if not value.has_next():
|
||||
return None
|
||||
page = value.next_page_number()
|
||||
|
@ -28,7 +28,7 @@ class PreviousPageField(serializers.Field):
|
|||
"""
|
||||
page_field = 'page'
|
||||
|
||||
def to_native(self, value):
|
||||
def to_representation(self, value):
|
||||
if not value.has_previous():
|
||||
return None
|
||||
page = value.previous_page_number()
|
||||
|
@ -37,7 +37,7 @@ class PreviousPageField(serializers.Field):
|
|||
return replace_query_param(url, self.page_field, page)
|
||||
|
||||
|
||||
class DefaultObjectSerializer(serializers.Field):
|
||||
class DefaultObjectSerializer(serializers.ReadOnlyField):
|
||||
"""
|
||||
If no object serializer is specified, then this serializer will be applied
|
||||
as the default.
|
||||
|
@ -49,25 +49,11 @@ class DefaultObjectSerializer(serializers.Field):
|
|||
super(DefaultObjectSerializer, self).__init__(source=source)
|
||||
|
||||
|
||||
class PaginationSerializerOptions(serializers.SerializerOptions):
|
||||
"""
|
||||
An object that stores the options that may be provided to a
|
||||
pagination serializer by using the inner `Meta` class.
|
||||
|
||||
Accessible on the instance as `serializer.opts`.
|
||||
"""
|
||||
def __init__(self, meta):
|
||||
super(PaginationSerializerOptions, self).__init__(meta)
|
||||
self.object_serializer_class = getattr(meta, 'object_serializer_class',
|
||||
DefaultObjectSerializer)
|
||||
|
||||
|
||||
class BasePaginationSerializer(serializers.Serializer):
|
||||
"""
|
||||
A base class for pagination serializers to inherit from,
|
||||
to make implementing custom serializers more easy.
|
||||
"""
|
||||
_options_class = PaginationSerializerOptions
|
||||
results_field = 'results'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -76,22 +62,22 @@ class BasePaginationSerializer(serializers.Serializer):
|
|||
"""
|
||||
super(BasePaginationSerializer, self).__init__(*args, **kwargs)
|
||||
results_field = self.results_field
|
||||
object_serializer = self.opts.object_serializer_class
|
||||
|
||||
if 'context' in kwargs:
|
||||
context_kwarg = {'context': kwargs['context']}
|
||||
else:
|
||||
context_kwarg = {}
|
||||
try:
|
||||
object_serializer = self.Meta.object_serializer_class
|
||||
except AttributeError:
|
||||
object_serializer = DefaultObjectSerializer
|
||||
|
||||
self.fields[results_field] = object_serializer(source='object_list',
|
||||
many=True,
|
||||
**context_kwarg)
|
||||
self.fields[results_field] = serializers.ListSerializer(
|
||||
child=object_serializer(),
|
||||
source='object_list'
|
||||
)
|
||||
|
||||
|
||||
class PaginationSerializer(BasePaginationSerializer):
|
||||
"""
|
||||
A default implementation of a pagination serializer.
|
||||
"""
|
||||
count = serializers.Field(source='paginator.count')
|
||||
count = serializers.ReadOnlyField(source='paginator.count')
|
||||
next = NextPageField(source='*')
|
||||
previous = PreviousPageField(source='*')
|
||||
|
|
|
@ -5,6 +5,7 @@ They give us a generic way of being able to handle various media types
|
|||
on the request, such as form content or json encoded data.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadhandler import StopFutureHandlers
|
||||
from django.http import QueryDict
|
||||
|
@ -48,7 +49,7 @@ class JSONParser(BaseParser):
|
|||
"""
|
||||
|
||||
media_type = 'application/json'
|
||||
renderer_class = renderers.UnicodeJSONRenderer
|
||||
renderer_class = renderers.JSONRenderer
|
||||
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
|
@ -132,7 +133,7 @@ class MultiPartParser(BaseParser):
|
|||
data, files = parser.parse()
|
||||
return DataAndFiles(data, files)
|
||||
except MultiPartParserError as exc:
|
||||
raise ParseError('Multipart form parse error - %s' % str(exc))
|
||||
raise ParseError('Multipart form parse error - %s' % six.text_type(exc))
|
||||
|
||||
|
||||
class XMLParser(BaseParser):
|
||||
|
|
|
@ -1,356 +1,164 @@
|
|||
"""
|
||||
Serializer fields that deal with relationships.
|
||||
|
||||
These fields allow you to specify the style that should be used to represent
|
||||
model relationships, including hyperlinks, primary keys, or slugs.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
|
||||
from django import forms
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.forms import widgets
|
||||
from django.forms.models import ModelChoiceIterator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.fields import Field, WritableField, get_component, is_simple_callable
|
||||
from rest_framework.compat import smart_text, urlparse
|
||||
from rest_framework.fields import get_attribute, empty, Field
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.compat import urlparse
|
||||
from rest_framework.compat import smart_text
|
||||
import warnings
|
||||
from rest_framework.utils import html
|
||||
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
|
||||
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch, Resolver404
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
# Relational fields
|
||||
class PKOnlyObject(object):
|
||||
def __init__(self, pk):
|
||||
self.pk = pk
|
||||
|
||||
# Not actually Writable, but subclasses may need to be.
|
||||
class RelatedField(WritableField):
|
||||
|
||||
class RelatedField(Field):
|
||||
def __init__(self, **kwargs):
|
||||
self.queryset = kwargs.pop('queryset', None)
|
||||
assert self.queryset is not None or kwargs.get('read_only', None), (
|
||||
'Relational field must provide a `queryset` argument, '
|
||||
'or set read_only=`True`.'
|
||||
)
|
||||
assert not (self.queryset is not None and kwargs.get('read_only', None)), (
|
||||
'Relational fields should not provide a `queryset` argument, '
|
||||
'when setting read_only=`True`.'
|
||||
)
|
||||
super(RelatedField, self).__init__(**kwargs)
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
# We override this method in order to automagically create
|
||||
# `ManyRelation` classes instead when `many=True` is set.
|
||||
if kwargs.pop('many', False):
|
||||
return ManyRelation(
|
||||
child_relation=cls(*args, **kwargs),
|
||||
read_only=kwargs.get('read_only', False)
|
||||
)
|
||||
return super(RelatedField, cls).__new__(cls, *args, **kwargs)
|
||||
|
||||
def run_validation(self, data=empty):
|
||||
# We force empty strings to None values for relational fields.
|
||||
if data == '':
|
||||
data = None
|
||||
return super(RelatedField, self).run_validation(data)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.queryset
|
||||
if isinstance(queryset, QuerySet):
|
||||
# Ensure queryset is re-evaluated whenever used.
|
||||
queryset = queryset.all()
|
||||
return queryset
|
||||
|
||||
def get_iterable(self, instance, source):
|
||||
relationship = get_attribute(instance, [source])
|
||||
return relationship.all() if (hasattr(relationship, 'all')) else relationship
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
return dict([
|
||||
(
|
||||
str(self.to_representation(item)),
|
||||
str(item)
|
||||
)
|
||||
for item in self.queryset.all()
|
||||
])
|
||||
|
||||
|
||||
class StringRelatedField(RelatedField):
|
||||
"""
|
||||
Base class for related model fields.
|
||||
|
||||
This represents a relationship using the unicode representation of the target.
|
||||
A read only field that represents its targets using their
|
||||
plain string representation.
|
||||
"""
|
||||
widget = widgets.Select
|
||||
many_widget = widgets.SelectMultiple
|
||||
form_field_class = forms.ChoiceField
|
||||
many_form_field_class = forms.MultipleChoiceField
|
||||
null_values = (None, '', 'None')
|
||||
|
||||
cache_choices = False
|
||||
empty_label = None
|
||||
read_only = True
|
||||
many = False
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['read_only'] = True
|
||||
super(StringRelatedField, self).__init__(**kwargs)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
queryset = kwargs.pop('queryset', None)
|
||||
self.many = kwargs.pop('many', self.many)
|
||||
if self.many:
|
||||
self.widget = self.many_widget
|
||||
self.form_field_class = self.many_form_field_class
|
||||
def to_representation(self, value):
|
||||
return six.text_type(value)
|
||||
|
||||
kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
|
||||
super(RelatedField, self).__init__(*args, **kwargs)
|
||||
|
||||
if not self.required:
|
||||
# Accessed in ModelChoiceIterator django/forms/models.py:1034
|
||||
# If set adds empty choice.
|
||||
self.empty_label = BLANK_CHOICE_DASH[0][1]
|
||||
|
||||
self.queryset = queryset
|
||||
|
||||
def initialize(self, parent, field_name):
|
||||
super(RelatedField, self).initialize(parent, field_name)
|
||||
if self.queryset is None and not self.read_only:
|
||||
manager = getattr(self.parent.opts.model, self.source or field_name)
|
||||
if hasattr(manager, 'related'): # Forward
|
||||
self.queryset = manager.related.model._default_manager.all()
|
||||
else: # Reverse
|
||||
self.queryset = manager.field.rel.to._default_manager.all()
|
||||
|
||||
# We need this stuff to make form choices work...
|
||||
|
||||
def prepare_value(self, obj):
|
||||
return self.to_native(obj)
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
"""
|
||||
Return a readable representation for use with eg. select widgets.
|
||||
"""
|
||||
desc = smart_text(obj)
|
||||
ident = smart_text(self.to_native(obj))
|
||||
if desc == ident:
|
||||
return desc
|
||||
return "%s - %s" % (desc, ident)
|
||||
|
||||
def _get_queryset(self):
|
||||
return self._queryset
|
||||
|
||||
def _set_queryset(self, queryset):
|
||||
self._queryset = queryset
|
||||
self.widget.choices = self.choices
|
||||
|
||||
queryset = property(_get_queryset, _set_queryset)
|
||||
|
||||
def _get_choices(self):
|
||||
# If self._choices is set, then somebody must have manually set
|
||||
# the property self.choices. In this case, just return self._choices.
|
||||
if hasattr(self, '_choices'):
|
||||
return self._choices
|
||||
|
||||
# Otherwise, execute the QuerySet in self.queryset to determine the
|
||||
# choices dynamically. Return a fresh ModelChoiceIterator that has not been
|
||||
# consumed. Note that we're instantiating a new ModelChoiceIterator *each*
|
||||
# time _get_choices() is called (and, thus, each time self.choices is
|
||||
# accessed) so that we can ensure the QuerySet has not been consumed. This
|
||||
# construct might look complicated but it allows for lazy evaluation of
|
||||
# the queryset.
|
||||
return ModelChoiceIterator(self)
|
||||
|
||||
def _set_choices(self, value):
|
||||
# Setting choices also sets the choices on the widget.
|
||||
# choices can be any iterable, but we call list() on it because
|
||||
# it will be consumed more than once.
|
||||
self._choices = self.widget.choices = list(value)
|
||||
|
||||
choices = property(_get_choices, _set_choices)
|
||||
|
||||
# Default value handling
|
||||
|
||||
def get_default_value(self):
|
||||
default = super(RelatedField, self).get_default_value()
|
||||
if self.many and default is None:
|
||||
return []
|
||||
return default
|
||||
|
||||
# Regular serializer stuff...
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
try:
|
||||
if self.source == '*':
|
||||
return self.to_native(obj)
|
||||
|
||||
source = self.source or field_name
|
||||
value = obj
|
||||
|
||||
for component in source.split('.'):
|
||||
if value is None:
|
||||
break
|
||||
value = get_component(value, component)
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if self.many:
|
||||
if is_simple_callable(getattr(value, 'all', None)):
|
||||
return [self.to_native(item) for item in value.all()]
|
||||
else:
|
||||
# Also support non-queryset iterables.
|
||||
# This allows us to also support plain lists of related items.
|
||||
return [self.to_native(item) for item in value]
|
||||
return self.to_native(value)
|
||||
|
||||
def field_from_native(self, data, files, field_name, into):
|
||||
if self.read_only:
|
||||
return
|
||||
|
||||
try:
|
||||
if self.many:
|
||||
try:
|
||||
# Form data
|
||||
value = data.getlist(field_name)
|
||||
if value == [''] or value == []:
|
||||
raise KeyError
|
||||
except AttributeError:
|
||||
# Non-form data
|
||||
value = data[field_name]
|
||||
else:
|
||||
value = data[field_name]
|
||||
except KeyError:
|
||||
if self.partial:
|
||||
return
|
||||
value = self.get_default_value()
|
||||
|
||||
if value in self.null_values:
|
||||
if self.required:
|
||||
raise ValidationError(self.error_messages['required'])
|
||||
into[(self.source or field_name)] = None
|
||||
elif self.many:
|
||||
into[(self.source or field_name)] = [self.from_native(item) for item in value]
|
||||
else:
|
||||
into[(self.source or field_name)] = self.from_native(value)
|
||||
|
||||
|
||||
# PrimaryKey relationships
|
||||
|
||||
class PrimaryKeyRelatedField(RelatedField):
|
||||
"""
|
||||
Represents a relationship as a pk value.
|
||||
"""
|
||||
read_only = False
|
||||
|
||||
default_error_messages = {
|
||||
'does_not_exist': _("Invalid pk '%s' - object does not exist."),
|
||||
'incorrect_type': _('Incorrect type. Expected pk value, received %s.'),
|
||||
'required': 'This field is required.',
|
||||
'does_not_exist': "Invalid pk '{pk_value}' - object does not exist.",
|
||||
'incorrect_type': 'Incorrect type. Expected pk value, received {data_type}.',
|
||||
}
|
||||
|
||||
# TODO: Remove these field hacks...
|
||||
def prepare_value(self, obj):
|
||||
return self.to_native(obj.pk)
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
"""
|
||||
Return a readable representation for use with eg. select widgets.
|
||||
"""
|
||||
desc = smart_text(obj)
|
||||
ident = smart_text(self.to_native(obj.pk))
|
||||
if desc == ident:
|
||||
return desc
|
||||
return "%s - %s" % (desc, ident)
|
||||
|
||||
# TODO: Possibly change this to just take `obj`, through prob less performant
|
||||
def to_native(self, pk):
|
||||
return pk
|
||||
|
||||
def from_native(self, data):
|
||||
if self.queryset is None:
|
||||
raise Exception('Writable related fields must include a `queryset` argument')
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
return self.queryset.get(pk=data)
|
||||
return self.get_queryset().get(pk=data)
|
||||
except ObjectDoesNotExist:
|
||||
msg = self.error_messages['does_not_exist'] % smart_text(data)
|
||||
raise ValidationError(msg)
|
||||
self.fail('does_not_exist', pk_value=data)
|
||||
except (TypeError, ValueError):
|
||||
received = type(data).__name__
|
||||
msg = self.error_messages['incorrect_type'] % received
|
||||
raise ValidationError(msg)
|
||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
if self.many:
|
||||
# To-many relationship
|
||||
|
||||
queryset = None
|
||||
if not self.source:
|
||||
# Prefer obj.serializable_value for performance reasons
|
||||
try:
|
||||
queryset = obj.serializable_value(field_name)
|
||||
except AttributeError:
|
||||
pass
|
||||
if queryset is None:
|
||||
# RelatedManager (reverse relationship)
|
||||
source = self.source or field_name
|
||||
queryset = obj
|
||||
for component in source.split('.'):
|
||||
if queryset is None:
|
||||
return []
|
||||
queryset = get_component(queryset, component)
|
||||
|
||||
# Forward relationship
|
||||
if is_simple_callable(getattr(queryset, 'all', None)):
|
||||
return [self.to_native(item.pk) for item in queryset.all()]
|
||||
else:
|
||||
# Also support non-queryset iterables.
|
||||
# This allows us to also support plain lists of related items.
|
||||
return [self.to_native(item.pk) for item in queryset]
|
||||
|
||||
# To-one relationship
|
||||
def get_attribute(self, instance):
|
||||
# We customize `get_attribute` here for performance reasons.
|
||||
# For relationships the instance will already have the pk of
|
||||
# the related object. We return this directly instead of returning the
|
||||
# object itself, which would require a database lookup.
|
||||
try:
|
||||
# Prefer obj.serializable_value for performance reasons
|
||||
pk = obj.serializable_value(self.source or field_name)
|
||||
return PKOnlyObject(pk=instance.serializable_value(self.source))
|
||||
except AttributeError:
|
||||
# RelatedObject (reverse relationship)
|
||||
try:
|
||||
pk = getattr(obj, self.source or field_name).pk
|
||||
except (ObjectDoesNotExist, AttributeError):
|
||||
return None
|
||||
return get_attribute(instance, [self.source])
|
||||
|
||||
# Forward relationship
|
||||
return self.to_native(pk)
|
||||
def get_iterable(self, instance, source):
|
||||
# For consistency with `get_attribute` we're using `serializable_value()`
|
||||
# here. Typically there won't be any difference, but some custom field
|
||||
# types might return a non-primative value for the pk otherwise.
|
||||
#
|
||||
# We could try to get smart with `values_list('pk', flat=True)`, which
|
||||
# would be better in some case, but would actually end up with *more*
|
||||
# queries if the developer is using `prefetch_related` across the
|
||||
# relationship.
|
||||
relationship = super(PrimaryKeyRelatedField, self).get_iterable(instance, source)
|
||||
return [
|
||||
PKOnlyObject(pk=item.serializable_value('pk'))
|
||||
for item in relationship
|
||||
]
|
||||
|
||||
def to_representation(self, value):
|
||||
return value.pk
|
||||
|
||||
# Slug relationships
|
||||
|
||||
class SlugRelatedField(RelatedField):
|
||||
"""
|
||||
Represents a relationship using a unique field on the target.
|
||||
"""
|
||||
read_only = False
|
||||
|
||||
default_error_messages = {
|
||||
'does_not_exist': _("Object with %s=%s does not exist."),
|
||||
'invalid': _('Invalid value.'),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.slug_field = kwargs.pop('slug_field', None)
|
||||
assert self.slug_field, 'slug_field is required'
|
||||
super(SlugRelatedField, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_native(self, obj):
|
||||
return getattr(obj, self.slug_field)
|
||||
|
||||
def from_native(self, data):
|
||||
if self.queryset is None:
|
||||
raise Exception('Writable related fields must include a `queryset` argument')
|
||||
|
||||
try:
|
||||
return self.queryset.get(**{self.slug_field: data})
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError(self.error_messages['does_not_exist'] %
|
||||
(self.slug_field, smart_text(data)))
|
||||
except (TypeError, ValueError):
|
||||
msg = self.error_messages['invalid']
|
||||
raise ValidationError(msg)
|
||||
|
||||
|
||||
# Hyperlinked relationships
|
||||
|
||||
class HyperlinkedRelatedField(RelatedField):
|
||||
"""
|
||||
Represents a relationship using hyperlinking.
|
||||
"""
|
||||
read_only = False
|
||||
lookup_field = 'pk'
|
||||
|
||||
default_error_messages = {
|
||||
'no_match': _('Invalid hyperlink - No URL match'),
|
||||
'incorrect_match': _('Invalid hyperlink - Incorrect URL match'),
|
||||
'configuration_error': _('Invalid hyperlink due to configuration error'),
|
||||
'does_not_exist': _("Invalid hyperlink - object does not exist."),
|
||||
'incorrect_type': _('Incorrect type. Expected url string, received %s.'),
|
||||
'required': 'This field is required.',
|
||||
'no_match': 'Invalid hyperlink - No URL match',
|
||||
'incorrect_match': 'Invalid hyperlink - Incorrect URL match.',
|
||||
'does_not_exist': 'Invalid hyperlink - Object does not exist.',
|
||||
'incorrect_type': 'Incorrect type. Expected URL string, received {data_type}.',
|
||||
}
|
||||
|
||||
# These are all deprecated
|
||||
pk_url_kwarg = 'pk'
|
||||
slug_field = 'slug'
|
||||
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.view_name = kwargs.pop('view_name')
|
||||
except KeyError:
|
||||
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
|
||||
|
||||
def __init__(self, view_name=None, **kwargs):
|
||||
assert view_name is not None, 'The `view_name` argument is required.'
|
||||
self.view_name = view_name
|
||||
self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
|
||||
self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
|
||||
self.format = kwargs.pop('format', None)
|
||||
|
||||
# These are deprecated
|
||||
if 'pk_url_kwarg' in kwargs:
|
||||
msg = 'pk_url_kwarg is deprecated. Use lookup_field instead.'
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
if 'slug_url_kwarg' in kwargs:
|
||||
msg = 'slug_url_kwarg is deprecated. Use lookup_field instead.'
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
if 'slug_field' in kwargs:
|
||||
msg = 'slug_field is deprecated. Use lookup_field instead.'
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
# We include these simply for dependency injection in tests.
|
||||
# We can't add them as class attributes or they would expect an
|
||||
# implicit `self` argument to be passed.
|
||||
self.reverse = reverse
|
||||
self.resolve = resolve
|
||||
|
||||
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
|
||||
self.slug_field = kwargs.pop('slug_field', self.slug_field)
|
||||
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
|
||||
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
|
||||
super(HyperlinkedRelatedField, self).__init__(**kwargs)
|
||||
|
||||
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
|
||||
def get_object(self, view_name, view_args, view_kwargs):
|
||||
"""
|
||||
Return the object corresponding to a matched URL.
|
||||
|
||||
Takes the matched URL conf arguments, and should return an
|
||||
object instance, or raise an `ObjectDoesNotExist` exception.
|
||||
"""
|
||||
lookup_value = view_kwargs[self.lookup_url_kwarg]
|
||||
lookup_kwargs = {self.lookup_field: lookup_value}
|
||||
return self.get_queryset().get(**lookup_kwargs)
|
||||
|
||||
def get_url(self, obj, view_name, request, format):
|
||||
"""
|
||||
|
@ -359,176 +167,48 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
|
||||
attributes are not configured to correctly match the URL conf.
|
||||
"""
|
||||
lookup_field = getattr(obj, self.lookup_field)
|
||||
kwargs = {self.lookup_field: lookup_field}
|
||||
# Unsaved objects will not yet have a valid URL.
|
||||
if obj.pk is None:
|
||||
return None
|
||||
|
||||
lookup_value = getattr(obj, self.lookup_field)
|
||||
kwargs = {self.lookup_url_kwarg: lookup_value}
|
||||
return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
|
||||
if self.pk_url_kwarg != 'pk':
|
||||
# Only try pk if it has been explicitly set.
|
||||
# Otherwise, the default `lookup_field = 'pk'` has us covered.
|
||||
pk = obj.pk
|
||||
kwargs = {self.pk_url_kwarg: pk}
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
|
||||
slug = getattr(obj, self.slug_field, None)
|
||||
if slug is not None:
|
||||
# Only try slug if it corresponds to an attribute on the object.
|
||||
kwargs = {self.slug_url_kwarg: slug}
|
||||
try:
|
||||
ret = reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
if self.slug_field == 'slug' and self.slug_url_kwarg == 'slug':
|
||||
# If the lookup succeeds using the default slug params,
|
||||
# then `slug_field` is being used implicitly, and we
|
||||
# we need to warn about the pending deprecation.
|
||||
msg = 'Implicit slug field hyperlinked fields are deprecated.' \
|
||||
'You should set `lookup_field=slug` on the HyperlinkedRelatedField.'
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
return ret
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
|
||||
raise NoReverseMatch()
|
||||
|
||||
def get_object(self, queryset, view_name, view_args, view_kwargs):
|
||||
"""
|
||||
Return the object corresponding to a matched URL.
|
||||
|
||||
Takes the matched URL conf arguments, and the queryset, and should
|
||||
return an object instance, or raise an `ObjectDoesNotExist` exception.
|
||||
"""
|
||||
lookup = view_kwargs.get(self.lookup_field, None)
|
||||
pk = view_kwargs.get(self.pk_url_kwarg, None)
|
||||
slug = view_kwargs.get(self.slug_url_kwarg, None)
|
||||
|
||||
if lookup is not None:
|
||||
filter_kwargs = {self.lookup_field: lookup}
|
||||
elif pk is not None:
|
||||
filter_kwargs = {'pk': pk}
|
||||
elif slug is not None:
|
||||
filter_kwargs = {self.slug_field: slug}
|
||||
else:
|
||||
raise ObjectDoesNotExist()
|
||||
|
||||
return queryset.get(**filter_kwargs)
|
||||
|
||||
def to_native(self, obj):
|
||||
view_name = self.view_name
|
||||
request = self.context.get('request', None)
|
||||
format = self.format or self.context.get('format', None)
|
||||
|
||||
assert request is not None, (
|
||||
"`HyperlinkedRelatedField` requires the request in the serializer "
|
||||
"context. Add `context={'request': request}` when instantiating "
|
||||
"the serializer."
|
||||
)
|
||||
|
||||
# If the object has not yet been saved then we cannot hyperlink to it.
|
||||
if getattr(obj, 'pk', None) is None:
|
||||
return
|
||||
|
||||
# Return the hyperlink, or error if incorrectly configured.
|
||||
try:
|
||||
return self.get_url(obj, view_name, request, format)
|
||||
except NoReverseMatch:
|
||||
msg = (
|
||||
'Could not resolve URL for hyperlinked relationship using '
|
||||
'view name "%s". You may have failed to include the related '
|
||||
'model in your API, or incorrectly configured the '
|
||||
'`lookup_field` attribute on this field.'
|
||||
)
|
||||
raise Exception(msg % view_name)
|
||||
|
||||
def from_native(self, value):
|
||||
# Convert URL -> model instance pk
|
||||
# TODO: Use values_list
|
||||
queryset = self.queryset
|
||||
if queryset is None:
|
||||
raise Exception('Writable related fields must include a `queryset` argument')
|
||||
|
||||
try:
|
||||
http_prefix = value.startswith(('http:', 'https:'))
|
||||
http_prefix = data.startswith(('http:', 'https:'))
|
||||
except AttributeError:
|
||||
msg = self.error_messages['incorrect_type']
|
||||
raise ValidationError(msg % type(value).__name__)
|
||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||
|
||||
if http_prefix:
|
||||
# If needed convert absolute URLs to relative path
|
||||
value = urlparse.urlparse(value).path
|
||||
data = urlparse.urlparse(data).path
|
||||
prefix = get_script_prefix()
|
||||
if value.startswith(prefix):
|
||||
value = '/' + value[len(prefix):]
|
||||
if data.startswith(prefix):
|
||||
data = '/' + data[len(prefix):]
|
||||
|
||||
try:
|
||||
match = resolve(value)
|
||||
except Exception:
|
||||
raise ValidationError(self.error_messages['no_match'])
|
||||
match = self.resolve(data)
|
||||
except Resolver404:
|
||||
self.fail('no_match')
|
||||
|
||||
if match.view_name != self.view_name:
|
||||
raise ValidationError(self.error_messages['incorrect_match'])
|
||||
self.fail('incorrect_match')
|
||||
|
||||
try:
|
||||
return self.get_object(queryset, match.view_name,
|
||||
match.args, match.kwargs)
|
||||
return self.get_object(match.view_name, match.args, match.kwargs)
|
||||
except (ObjectDoesNotExist, TypeError, ValueError):
|
||||
raise ValidationError(self.error_messages['does_not_exist'])
|
||||
self.fail('does_not_exist')
|
||||
|
||||
|
||||
class HyperlinkedIdentityField(Field):
|
||||
"""
|
||||
Represents the instance, or a property on the instance, using hyperlinking.
|
||||
"""
|
||||
lookup_field = 'pk'
|
||||
read_only = True
|
||||
|
||||
# These are all deprecated
|
||||
pk_url_kwarg = 'pk'
|
||||
slug_field = 'slug'
|
||||
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.view_name = kwargs.pop('view_name')
|
||||
except KeyError:
|
||||
msg = "HyperlinkedIdentityField requires 'view_name' argument"
|
||||
raise ValueError(msg)
|
||||
|
||||
self.format = kwargs.pop('format', None)
|
||||
lookup_field = kwargs.pop('lookup_field', None)
|
||||
self.lookup_field = lookup_field or self.lookup_field
|
||||
|
||||
# These are deprecated
|
||||
if 'pk_url_kwarg' in kwargs:
|
||||
msg = 'pk_url_kwarg is deprecated. Use lookup_field instead.'
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
if 'slug_url_kwarg' in kwargs:
|
||||
msg = 'slug_url_kwarg is deprecated. Use lookup_field instead.'
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
if 'slug_field' in kwargs:
|
||||
msg = 'slug_field is deprecated. Use lookup_field instead.'
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
|
||||
self.slug_field = kwargs.pop('slug_field', self.slug_field)
|
||||
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
|
||||
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
|
||||
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
|
||||
|
||||
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
def to_representation(self, value):
|
||||
request = self.context.get('request', None)
|
||||
format = self.context.get('format', None)
|
||||
view_name = self.view_name
|
||||
|
||||
assert request is not None, (
|
||||
"`HyperlinkedIdentityField` requires the request in the serializer"
|
||||
"`%s` requires the request in the serializer"
|
||||
" context. Add `context={'request': request}` when instantiating "
|
||||
"the serializer."
|
||||
"the serializer." % self.__class__.__name__
|
||||
)
|
||||
|
||||
# By default use whatever format is given for the current context
|
||||
|
@ -545,7 +225,7 @@ class HyperlinkedIdentityField(Field):
|
|||
|
||||
# Return the hyperlink, or error if incorrectly configured.
|
||||
try:
|
||||
return self.get_url(obj, view_name, request, format)
|
||||
return self.get_url(value, self.view_name, request, format)
|
||||
except NoReverseMatch:
|
||||
msg = (
|
||||
'Could not resolve URL for hyperlinked relationship using '
|
||||
|
@ -553,43 +233,105 @@ class HyperlinkedIdentityField(Field):
|
|||
'model in your API, or incorrectly configured the '
|
||||
'`lookup_field` attribute on this field.'
|
||||
)
|
||||
raise Exception(msg % view_name)
|
||||
raise ImproperlyConfigured(msg % self.view_name)
|
||||
|
||||
def get_url(self, obj, view_name, request, format):
|
||||
"""
|
||||
Given an object, return the URL that hyperlinks to the object.
|
||||
|
||||
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
|
||||
attributes are not configured to correctly match the URL conf.
|
||||
"""
|
||||
lookup_field = getattr(obj, self.lookup_field, None)
|
||||
kwargs = {self.lookup_field: lookup_field}
|
||||
class HyperlinkedIdentityField(HyperlinkedRelatedField):
|
||||
"""
|
||||
A read-only field that represents the identity URL for an object, itself.
|
||||
|
||||
# Handle unsaved object case
|
||||
if lookup_field is None:
|
||||
return None
|
||||
This is in contrast to `HyperlinkedRelatedField` which represents the
|
||||
URL of relationships to other objects.
|
||||
"""
|
||||
|
||||
def __init__(self, view_name=None, **kwargs):
|
||||
assert view_name is not None, 'The `view_name` argument is required.'
|
||||
kwargs['read_only'] = True
|
||||
kwargs['source'] = '*'
|
||||
super(HyperlinkedIdentityField, self).__init__(view_name, **kwargs)
|
||||
|
||||
|
||||
class SlugRelatedField(RelatedField):
|
||||
"""
|
||||
A read-write field the represents the target of the relationship
|
||||
by a unique 'slug' attribute.
|
||||
"""
|
||||
|
||||
default_error_messages = {
|
||||
'does_not_exist': _("Object with {slug_name}={value} does not exist."),
|
||||
'invalid': _('Invalid value.'),
|
||||
}
|
||||
|
||||
def __init__(self, slug_field=None, **kwargs):
|
||||
assert slug_field is not None, 'The `slug_field` argument is required.'
|
||||
self.slug_field = slug_field
|
||||
super(SlugRelatedField, self).__init__(**kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
return self.get_queryset().get(**{self.slug_field: data})
|
||||
except ObjectDoesNotExist:
|
||||
self.fail('does_not_exist', slug_name=self.slug_field, value=smart_text(data))
|
||||
except (TypeError, ValueError):
|
||||
self.fail('invalid')
|
||||
|
||||
if self.pk_url_kwarg != 'pk':
|
||||
# Only try pk lookup if it has been explicitly set.
|
||||
# Otherwise, the default `lookup_field = 'pk'` has us covered.
|
||||
kwargs = {self.pk_url_kwarg: obj.pk}
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
def to_representation(self, obj):
|
||||
return getattr(obj, self.slug_field)
|
||||
|
||||
slug = getattr(obj, self.slug_field, None)
|
||||
if slug:
|
||||
# Only use slug lookup if a slug field exists on the model
|
||||
kwargs = {self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
|
||||
raise NoReverseMatch()
|
||||
class ManyRelation(Field):
|
||||
"""
|
||||
Relationships with `many=True` transparently get coerced into instead being
|
||||
a ManyRelation with a child relationship.
|
||||
|
||||
The `ManyRelation` class is responsible for handling iterating through
|
||||
the values and passing each one to the child relationship.
|
||||
|
||||
You shouldn't need to be using this class directly yourself.
|
||||
"""
|
||||
initial = []
|
||||
default_empty_html = []
|
||||
|
||||
def __init__(self, child_relation=None, *args, **kwargs):
|
||||
self.child_relation = child_relation
|
||||
assert child_relation is not None, '`child_relation` is a required argument.'
|
||||
super(ManyRelation, self).__init__(*args, **kwargs)
|
||||
self.child_relation.bind(field_name='', parent=self)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
# We override the default field access in order to support
|
||||
# lists in HTML forms.
|
||||
if html.is_html_input(dictionary):
|
||||
return dictionary.getlist(self.field_name)
|
||||
return dictionary.get(self.field_name, empty)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return [
|
||||
self.child_relation.to_internal_value(item)
|
||||
for item in data
|
||||
]
|
||||
|
||||
def get_attribute(self, instance):
|
||||
return self.child_relation.get_iterable(instance, self.source)
|
||||
|
||||
def to_representation(self, iterable):
|
||||
return [
|
||||
self.child_relation.to_representation(value)
|
||||
for value in iterable
|
||||
]
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
queryset = self.child_relation.queryset
|
||||
iterable = queryset.all() if (hasattr(queryset, 'all')) else queryset
|
||||
items_and_representations = [
|
||||
(item, self.child_relation.to_representation(item))
|
||||
for item in iterable
|
||||
]
|
||||
return dict([
|
||||
(
|
||||
str(item_representation),
|
||||
str(item) + ' - ' + str(item_representation)
|
||||
)
|
||||
for item, item_representation in items_and_representations
|
||||
])
|
||||
|
|
|
@ -13,17 +13,24 @@ import django
|
|||
from django import forms
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http.multipartparser import parse_header
|
||||
from django.template import RequestContext, loader, Template
|
||||
from django.template import Context, RequestContext, loader, Template
|
||||
from django.test.client import encode_multipart
|
||||
from django.utils import six
|
||||
from django.utils.xmlutils import SimplerXMLGenerator
|
||||
from rest_framework.compat import StringIO, smart_text, yaml
|
||||
from rest_framework import exceptions, serializers, status, VERSION
|
||||
from rest_framework.compat import (
|
||||
SHORT_SEPARATORS, LONG_SEPARATORS, StringIO, smart_text, yaml
|
||||
)
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.request import is_form_media_type, override_method
|
||||
from rest_framework.utils import encoders
|
||||
from rest_framework.utils.breadcrumbs import get_breadcrumbs
|
||||
from rest_framework import exceptions, status, VERSION
|
||||
from rest_framework.utils.field_mapping import ClassLookupDict
|
||||
|
||||
|
||||
def zero_as_none(value):
|
||||
return None if value == 0 else value
|
||||
|
||||
|
||||
class BaseRenderer(object):
|
||||
|
@ -44,13 +51,13 @@ class BaseRenderer(object):
|
|||
class JSONRenderer(BaseRenderer):
|
||||
"""
|
||||
Renderer which serializes to JSON.
|
||||
Applies JSON's backslash-u character escaping for non-ascii characters.
|
||||
"""
|
||||
|
||||
media_type = 'application/json'
|
||||
format = 'json'
|
||||
encoder_class = encoders.JSONEncoder
|
||||
ensure_ascii = True
|
||||
ensure_ascii = not api_settings.UNICODE_JSON
|
||||
compact = api_settings.COMPACT_JSON
|
||||
|
||||
# We don't set a charset because JSON is a binary encoding,
|
||||
# that can be encoded as utf-8, utf-16 or utf-32.
|
||||
|
@ -62,9 +69,10 @@ class JSONRenderer(BaseRenderer):
|
|||
if accepted_media_type:
|
||||
# If the media type looks like 'application/json; indent=4',
|
||||
# then pretty print the result.
|
||||
# Note that we coerce `indent=0` into `indent=None`.
|
||||
base_media_type, params = parse_header(accepted_media_type.encode('ascii'))
|
||||
try:
|
||||
return max(min(int(params['indent']), 8), 0)
|
||||
return zero_as_none(max(min(int(params['indent']), 8), 0))
|
||||
except (KeyError, ValueError, TypeError):
|
||||
pass
|
||||
|
||||
|
@ -81,10 +89,12 @@ class JSONRenderer(BaseRenderer):
|
|||
|
||||
renderer_context = renderer_context or {}
|
||||
indent = self.get_indent(accepted_media_type, renderer_context)
|
||||
separators = SHORT_SEPARATORS if (indent is None and self.compact) else LONG_SEPARATORS
|
||||
|
||||
ret = json.dumps(
|
||||
data, cls=self.encoder_class,
|
||||
indent=indent, ensure_ascii=self.ensure_ascii
|
||||
indent=indent, ensure_ascii=self.ensure_ascii,
|
||||
separators=separators
|
||||
)
|
||||
|
||||
# On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,
|
||||
|
@ -96,14 +106,6 @@ class JSONRenderer(BaseRenderer):
|
|||
return ret
|
||||
|
||||
|
||||
class UnicodeJSONRenderer(JSONRenderer):
|
||||
ensure_ascii = False
|
||||
"""
|
||||
Renderer which serializes to JSON.
|
||||
Does *not* apply JSON's character escaping for non-ascii characters.
|
||||
"""
|
||||
|
||||
|
||||
class JSONPRenderer(JSONRenderer):
|
||||
"""
|
||||
Renderer which serializes to json,
|
||||
|
@ -121,7 +123,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):
|
||||
|
@ -196,7 +198,7 @@ class YAMLRenderer(BaseRenderer):
|
|||
format = 'yaml'
|
||||
encoder = encoders.SafeDumper
|
||||
charset = 'utf-8'
|
||||
ensure_ascii = True
|
||||
ensure_ascii = False
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
|
@ -210,14 +212,6 @@ class YAMLRenderer(BaseRenderer):
|
|||
return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder, allow_unicode=not self.ensure_ascii)
|
||||
|
||||
|
||||
class UnicodeYAMLRenderer(YAMLRenderer):
|
||||
"""
|
||||
Renderer which serializes to YAML.
|
||||
Does *not* apply character escaping for non-ascii characters.
|
||||
"""
|
||||
ensure_ascii = False
|
||||
|
||||
|
||||
class TemplateHTMLRenderer(BaseRenderer):
|
||||
"""
|
||||
An HTML renderer for use with templates.
|
||||
|
@ -347,18 +341,103 @@ class HTMLFormRenderer(BaseRenderer):
|
|||
"""
|
||||
media_type = 'text/html'
|
||||
format = 'form'
|
||||
template = 'rest_framework/form.html'
|
||||
charset = 'utf-8'
|
||||
template_pack = 'rest_framework/horizontal/'
|
||||
base_template = 'form.html'
|
||||
|
||||
default_style = ClassLookupDict({
|
||||
serializers.Field: {
|
||||
'base_template': 'input.html',
|
||||
'input_type': 'text'
|
||||
},
|
||||
serializers.EmailField: {
|
||||
'base_template': 'input.html',
|
||||
'input_type': 'email'
|
||||
},
|
||||
serializers.URLField: {
|
||||
'base_template': 'input.html',
|
||||
'input_type': 'url'
|
||||
},
|
||||
serializers.IntegerField: {
|
||||
'base_template': 'input.html',
|
||||
'input_type': 'number'
|
||||
},
|
||||
serializers.DateTimeField: {
|
||||
'base_template': 'input.html',
|
||||
'input_type': 'datetime-local'
|
||||
},
|
||||
serializers.DateField: {
|
||||
'base_template': 'input.html',
|
||||
'input_type': 'date'
|
||||
},
|
||||
serializers.TimeField: {
|
||||
'base_template': 'input.html',
|
||||
'input_type': 'time'
|
||||
},
|
||||
serializers.BooleanField: {
|
||||
'base_template': 'checkbox.html'
|
||||
},
|
||||
serializers.ChoiceField: {
|
||||
'base_template': 'select.html', # Also valid: 'radio.html'
|
||||
},
|
||||
serializers.MultipleChoiceField: {
|
||||
'base_template': 'select_multiple.html', # Also valid: 'checkbox_multiple.html'
|
||||
},
|
||||
serializers.ManyRelation: {
|
||||
'base_template': 'select_multiple.html', # Also valid: 'checkbox_multiple.html'
|
||||
},
|
||||
serializers.Serializer: {
|
||||
'base_template': 'fieldset.html'
|
||||
},
|
||||
serializers.ListSerializer: {
|
||||
'base_template': 'list_fieldset.html'
|
||||
}
|
||||
})
|
||||
|
||||
def render_field(self, field, parent_style):
|
||||
style = dict(self.default_style[field])
|
||||
style.update(field.style)
|
||||
if 'template_pack' not in style:
|
||||
style['template_pack'] = parent_style.get('template_pack', self.template_pack)
|
||||
style['renderer'] = self
|
||||
|
||||
if style.get('input_type') == 'datetime-local' and isinstance(field.value, six.text_type):
|
||||
field.value = field.value.rstrip('Z')
|
||||
|
||||
if 'template' in style:
|
||||
template_name = style['template']
|
||||
else:
|
||||
template_name = style['template_pack'].strip('/') + '/' + style['base_template']
|
||||
|
||||
template = loader.get_template(template_name)
|
||||
context = Context({'field': field, 'style': style})
|
||||
return template.render(context)
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
Render serializer data and return an HTML form, as a string.
|
||||
"""
|
||||
form = data.serializer
|
||||
meta = getattr(form, 'Meta', None)
|
||||
style = getattr(meta, 'style', {})
|
||||
if 'template_pack' not in style:
|
||||
style['template_pack'] = self.template_pack
|
||||
if 'base_template' not in style:
|
||||
style['base_template'] = self.base_template
|
||||
style['renderer'] = self
|
||||
|
||||
if 'template' in style:
|
||||
template_name = style['template']
|
||||
else:
|
||||
template_name = style['template_pack'].strip('/') + '/' + style['base_template']
|
||||
|
||||
renderer_context = renderer_context or {}
|
||||
request = renderer_context['request']
|
||||
|
||||
template = loader.get_template(self.template)
|
||||
context = RequestContext(request, {'form': data})
|
||||
template = loader.get_template(template_name)
|
||||
context = RequestContext(request, {
|
||||
'form': form,
|
||||
'style': style
|
||||
})
|
||||
return template.render(context)
|
||||
|
||||
|
||||
|
@ -426,27 +505,29 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
return False # Doesn't have permissions
|
||||
return True
|
||||
|
||||
def get_rendered_html_form(self, view, method, request):
|
||||
def get_rendered_html_form(self, data, view, method, request):
|
||||
"""
|
||||
Return a string representing a rendered HTML form, possibly bound to
|
||||
either the input or output data.
|
||||
|
||||
In the absence of the View having an associated form then return None.
|
||||
"""
|
||||
serializer = getattr(data, 'serializer', None)
|
||||
if serializer and not getattr(serializer, 'many', False):
|
||||
instance = getattr(serializer, 'instance', None)
|
||||
else:
|
||||
instance = None
|
||||
|
||||
if request.method == method:
|
||||
try:
|
||||
data = request.DATA
|
||||
files = request.FILES
|
||||
data = request.data
|
||||
except ParseError:
|
||||
data = None
|
||||
files = None
|
||||
else:
|
||||
data = None
|
||||
files = None
|
||||
|
||||
with override_method(view, request, method) as request:
|
||||
obj = getattr(view, 'object', None)
|
||||
if not self.show_form_for_method(view, method, request, obj):
|
||||
if not self.show_form_for_method(view, method, request, instance):
|
||||
return
|
||||
|
||||
if method in ('DELETE', 'OPTIONS'):
|
||||
|
@ -458,19 +539,24 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
):
|
||||
return
|
||||
|
||||
serializer = view.get_serializer(instance=obj, data=data, files=files)
|
||||
serializer.is_valid()
|
||||
data = serializer.data
|
||||
|
||||
serializer = view.get_serializer(instance=instance, data=data)
|
||||
if data is not None:
|
||||
serializer.is_valid()
|
||||
form_renderer = self.form_renderer_class()
|
||||
return form_renderer.render(data, self.accepted_media_type, self.renderer_context)
|
||||
return form_renderer.render(serializer.data, self.accepted_media_type, self.renderer_context)
|
||||
|
||||
def get_raw_data_form(self, view, method, request):
|
||||
def get_raw_data_form(self, data, view, method, request):
|
||||
"""
|
||||
Returns a form that allows for arbitrary content types to be tunneled
|
||||
via standard HTML forms.
|
||||
(Which are typically application/x-www-form-urlencoded)
|
||||
"""
|
||||
serializer = getattr(data, 'serializer', None)
|
||||
if serializer and not getattr(serializer, 'many', False):
|
||||
instance = getattr(serializer, 'instance', None)
|
||||
else:
|
||||
instance = None
|
||||
|
||||
with override_method(view, request, method) as request:
|
||||
# If we're not using content overloading there's no point in
|
||||
# supplying a generic form, as the view won't treat the form's
|
||||
|
@ -480,8 +566,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
return None
|
||||
|
||||
# Check permissions
|
||||
obj = getattr(view, 'object', None)
|
||||
if not self.show_form_for_method(view, method, request, obj):
|
||||
if not self.show_form_for_method(view, method, request, instance):
|
||||
return
|
||||
|
||||
# If possible, serialize the initial content for the generic form
|
||||
|
@ -492,8 +577,8 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
# corresponding renderer that can be used to render the data.
|
||||
|
||||
# Get a read-only version of the serializer
|
||||
serializer = view.get_serializer(instance=obj)
|
||||
if obj is None:
|
||||
serializer = view.get_serializer(instance=instance)
|
||||
if instance is None:
|
||||
for name, field in serializer.fields.items():
|
||||
if getattr(field, 'read_only', None):
|
||||
del serializer.fields[name]
|
||||
|
@ -553,9 +638,9 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
|
||||
renderer = self.get_default_renderer(view)
|
||||
|
||||
raw_data_post_form = self.get_raw_data_form(view, 'POST', request)
|
||||
raw_data_put_form = self.get_raw_data_form(view, 'PUT', request)
|
||||
raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request)
|
||||
raw_data_post_form = self.get_raw_data_form(data, view, 'POST', request)
|
||||
raw_data_put_form = self.get_raw_data_form(data, view, 'PUT', request)
|
||||
raw_data_patch_form = self.get_raw_data_form(data, view, 'PATCH', request)
|
||||
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
|
||||
|
||||
response_headers = dict(response.items())
|
||||
|
@ -579,10 +664,10 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
|
||||
'response_headers': response_headers,
|
||||
|
||||
'put_form': self.get_rendered_html_form(view, 'PUT', request),
|
||||
'post_form': self.get_rendered_html_form(view, 'POST', request),
|
||||
'delete_form': self.get_rendered_html_form(view, 'DELETE', request),
|
||||
'options_form': self.get_rendered_html_form(view, 'OPTIONS', request),
|
||||
'put_form': self.get_rendered_html_form(data, view, 'PUT', request),
|
||||
'post_form': self.get_rendered_html_form(data, view, 'POST', request),
|
||||
'delete_form': self.get_rendered_html_form(data, view, 'DELETE', request),
|
||||
'options_form': self.get_rendered_html_form(data, view, 'OPTIONS', request),
|
||||
|
||||
'raw_data_put_form': raw_data_put_form,
|
||||
'raw_data_post_form': raw_data_post_form,
|
||||
|
|
|
@ -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,10 +13,12 @@ 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
|
||||
from rest_framework.settings import api_settings
|
||||
import warnings
|
||||
|
||||
|
||||
def is_form_media_type(media_type):
|
||||
|
@ -58,6 +60,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 +93,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 +145,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 +199,30 @@ 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.
|
||||
"""
|
||||
warnings.warn(
|
||||
"`request.QUERY_PARAMS` is pending deprecation. Use `request.query_params` instead.",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=1
|
||||
)
|
||||
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):
|
||||
"""
|
||||
|
@ -200,6 +231,11 @@ class Request(object):
|
|||
Similar to usual behaviour of `request.POST`, except that it handles
|
||||
arbitrary parsers, and also works on methods other than POST (eg PUT).
|
||||
"""
|
||||
warnings.warn(
|
||||
"`request.DATA` is pending deprecation. Use `request.data` instead.",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=1
|
||||
)
|
||||
if not _hasattr(self, '_data'):
|
||||
self._load_data_and_files()
|
||||
return self._data
|
||||
|
@ -212,6 +248,11 @@ class Request(object):
|
|||
Similar to usual behaviour of `request.FILES`, except that it handles
|
||||
arbitrary parsers, and also works on methods other than POST (eg PUT).
|
||||
"""
|
||||
warnings.warn(
|
||||
"`request.FILES` is pending deprecation. Use `request.data` instead.",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=1
|
||||
)
|
||||
if not _hasattr(self, '_files'):
|
||||
self._load_data_and_files()
|
||||
return self._files
|
||||
|
@ -272,6 +313,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 +378,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 +396,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 +426,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
|
||||
|
|
|
@ -3,6 +3,7 @@ Provide reverse functions that return fully qualified URLs
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
from django.core.urlresolvers import reverse as django_reverse
|
||||
from django.utils import six
|
||||
from django.utils.functional import lazy
|
||||
|
||||
|
||||
|
@ -20,4 +21,4 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra
|
|||
return url
|
||||
|
||||
|
||||
reverse_lazy = lazy(reverse, str)
|
||||
reverse_lazy = lazy(reverse, six.text_type)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -45,6 +45,7 @@ DEFAULTS = {
|
|||
),
|
||||
'DEFAULT_THROTTLE_CLASSES': (),
|
||||
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
|
||||
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
|
||||
|
||||
# Genric view behavior
|
||||
'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer',
|
||||
|
@ -77,6 +78,7 @@ DEFAULTS = {
|
|||
|
||||
# Exception handling
|
||||
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
|
||||
'NON_FIELD_ERRORS_KEY': 'non_field_errors',
|
||||
|
||||
# Testing
|
||||
'TEST_REQUEST_RENDERER_CLASSES': (
|
||||
|
@ -96,24 +98,20 @@ DEFAULTS = {
|
|||
'URL_FIELD_NAME': 'url',
|
||||
|
||||
# Input and output formats
|
||||
'DATE_INPUT_FORMATS': (
|
||||
ISO_8601,
|
||||
),
|
||||
'DATE_FORMAT': None,
|
||||
'DATE_FORMAT': ISO_8601,
|
||||
'DATE_INPUT_FORMATS': (ISO_8601,),
|
||||
|
||||
'DATETIME_INPUT_FORMATS': (
|
||||
ISO_8601,
|
||||
),
|
||||
'DATETIME_FORMAT': None,
|
||||
'DATETIME_FORMAT': ISO_8601,
|
||||
'DATETIME_INPUT_FORMATS': (ISO_8601,),
|
||||
|
||||
'TIME_INPUT_FORMATS': (
|
||||
ISO_8601,
|
||||
),
|
||||
'TIME_FORMAT': None,
|
||||
|
||||
# Pending deprecation
|
||||
'FILTER_BACKEND': None,
|
||||
'TIME_FORMAT': ISO_8601,
|
||||
'TIME_INPUT_FORMATS': (ISO_8601,),
|
||||
|
||||
# Encoding
|
||||
'UNICODE_JSON': True,
|
||||
'COMPACT_JSON': True,
|
||||
'COERCE_DECIMAL_TO_STRING': True,
|
||||
'UPLOADED_FILES_USE_URL': True
|
||||
}
|
||||
|
||||
|
||||
|
@ -125,11 +123,11 @@ IMPORT_STRINGS = (
|
|||
'DEFAULT_PERMISSION_CLASSES',
|
||||
'DEFAULT_THROTTLE_CLASSES',
|
||||
'DEFAULT_CONTENT_NEGOTIATION_CLASS',
|
||||
'DEFAULT_METADATA_CLASS',
|
||||
'DEFAULT_MODEL_SERIALIZER_CLASS',
|
||||
'DEFAULT_PAGINATION_SERIALIZER_CLASS',
|
||||
'DEFAULT_FILTER_BACKENDS',
|
||||
'EXCEPTION_HANDLER',
|
||||
'FILTER_BACKEND',
|
||||
'TEST_REQUEST_RENDERER_CLASSES',
|
||||
'UNAUTHENTICATED_USER',
|
||||
'UNAUTHENTICATED_TOKEN',
|
||||
|
@ -196,15 +194,9 @@ class APISettings(object):
|
|||
if val and attr in self.import_strings:
|
||||
val = perform_import(val, attr)
|
||||
|
||||
self.validate_setting(attr, val)
|
||||
|
||||
# Cache the result
|
||||
setattr(self, attr, val)
|
||||
return val
|
||||
|
||||
def validate_setting(self, attr, val):
|
||||
if attr == 'FILTER_BACKEND' and val is not None:
|
||||
# Make sure we can initialize the class
|
||||
val()
|
||||
|
||||
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
|
||||
|
|
|
@ -10,6 +10,12 @@ a single block in the template.
|
|||
background: transparent;
|
||||
border-top-color: transparent;
|
||||
padding-top: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#generic-content-form textarea {
|
||||
font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.navbar-inverse .brand a {
|
||||
|
@ -29,7 +35,7 @@ a single block in the template.
|
|||
z-index: 3;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner {
|
||||
.navbar {
|
||||
background: #2C2C2C;
|
||||
color: white;
|
||||
border: none;
|
||||
|
@ -37,7 +43,7 @@ a single block in the template.
|
|||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover {
|
||||
.navbar .nav li, .navbar .nav li a, .navbar .brand:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
@ -45,11 +51,11 @@ a single block in the template.
|
|||
background: #2C2C2C;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li {
|
||||
.navbar .dropdown-menu li a, .navbar .dropdown-menu li {
|
||||
color: #A30000;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner .dropdown-menu li a:hover {
|
||||
.navbar .dropdown-menu li a:hover {
|
||||
background: #EEEEEE;
|
||||
color: #C20000;
|
||||
}
|
||||
|
@ -61,10 +67,10 @@ html {
|
|||
background: none;
|
||||
}
|
||||
|
||||
body, .navbar .navbar-inner .container-fluid {
|
||||
/*body, .navbar .container-fluid {
|
||||
max-width: 1150px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}*/
|
||||
|
||||
body {
|
||||
background: url("../img/grid.png") repeat-x;
|
||||
|
@ -109,10 +115,6 @@ html, body {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.well form .help-block {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
border: 0;
|
||||
}
|
||||
|
@ -167,7 +169,7 @@ footer a:hover {
|
|||
.page-header {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0px;
|
||||
margin-bottom: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* custom general page styles */
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -33,7 +33,11 @@ h2, h3 {
|
|||
}
|
||||
|
||||
ul.breadcrumb {
|
||||
margin: 80px 0 0 0;
|
||||
margin: 70px 0 0 0;
|
||||
}
|
||||
|
||||
.breadcrumb li.active a {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
form select, form input, form textarea {
|
||||
|
@ -67,5 +71,4 @@ pre {
|
|||
.page-header {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -26,44 +26,42 @@
|
|||
</head>
|
||||
|
||||
{% block body %}
|
||||
<body class="{% block bodyclass %}{% endblock %} container">
|
||||
<body class="{% block bodyclass %}{% endblock %}">
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
{% block navbar %}
|
||||
<div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
|
||||
<div class="navbar-inner">
|
||||
<div class="container-fluid">
|
||||
<span>
|
||||
{% block branding %}
|
||||
<a class='brand' rel="nofollow" href='http://www.django-rest-framework.org'>
|
||||
Django REST framework <span class="version">{{ version }}</span>
|
||||
</a>
|
||||
{% endblock %}
|
||||
</span>
|
||||
<ul class="nav pull-right">
|
||||
{% block userlinks %}
|
||||
{% if user.is_authenticated %}
|
||||
{% optional_logout request user %}
|
||||
{% else %}
|
||||
{% optional_login request %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar navbar-static-top {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
|
||||
<div class="container">
|
||||
<span>
|
||||
{% block branding %}
|
||||
<a class='navbar-brand' rel="nofollow" href='http://www.django-rest-framework.org'>
|
||||
Django REST framework <span class="version">{{ version }}</span>
|
||||
</a>
|
||||
{% endblock %}
|
||||
</span>
|
||||
<ul class="nav navbar-nav pull-right">
|
||||
{% block userlinks %}
|
||||
{% if user.is_authenticated %}
|
||||
{% optional_logout request user %}
|
||||
{% else %}
|
||||
{% optional_login request %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<div class="container">
|
||||
{% block breadcrumbs %}
|
||||
<ul class="breadcrumb">
|
||||
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
|
||||
<li>
|
||||
<a href="{{ breadcrumb_url }}" {% if forloop.last %}class="active"{% endif %}>
|
||||
{{ breadcrumb_name }}
|
||||
</a>
|
||||
{% if not forloop.last %}<span class="divider">›</span>{% endif %}
|
||||
</li>
|
||||
{% if forloop.last %}
|
||||
<li class="active"><a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
@ -154,7 +152,7 @@
|
|||
<div class="tab-pane" id="post-object-form">
|
||||
{% with form=post_form %}
|
||||
<form action="{{ request.get_full_path }}"
|
||||
method="POST" enctype="multipart/form-data" class="form-horizontal">
|
||||
method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
|
||||
<fieldset>
|
||||
{{ post_form }}
|
||||
<div class="form-actions">
|
||||
|
@ -199,7 +197,7 @@
|
|||
{% if put_form %}
|
||||
<div class="tab-pane" id="put-object-form">
|
||||
<form action="{{ request.get_full_path }}"
|
||||
method="POST" enctype="multipart/form-data" class="form-horizontal">
|
||||
method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
|
||||
<fieldset>
|
||||
{{ put_form }}
|
||||
<div class="form-actions">
|
||||
|
@ -238,6 +236,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
<!-- END Content -->
|
||||
</div><!-- /.container -->
|
||||
|
||||
<footer>
|
||||
{% block footer %}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
{% load rest_framework %}
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
{% for field in form.fields.values %}
|
||||
{% if not field.read_only %}
|
||||
<div class="control-group {% if field.errors %}error{% endif %}">
|
||||
{{ field.label_tag|add_class:"control-label" }}
|
||||
<div class="controls">
|
||||
{{ field.widget_html }}
|
||||
{% if field.help_text %}<span class="help-block">{{ field.help_text }}</span>{% endif %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
|
@ -0,0 +1,16 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="{{ field.name }}" value="true" {% if field.value %}checked{% endif %}>
|
||||
{% if field.label %}{{ field.label }}{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,30 @@
|
|||
<div class="form-group">
|
||||
{% if field.label %}
|
||||
<label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
|
||||
{% endif %}
|
||||
<div class="col-sm-10">
|
||||
{% if style.inline %}
|
||||
{% for key, text in field.choices.items %}
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}>
|
||||
{{ text }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for key, text in field.choices.items %}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}>
|
||||
{{ text }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,11 @@
|
|||
{% load rest_framework %}
|
||||
<fieldset>
|
||||
{% if field.label %}
|
||||
<div class="form-group" style="border-bottom: 1px solid #e5e5e5">
|
||||
<legend class="control-label col-sm-2 {% if style.hide_label %}sr-only{% endif %}" style="border-bottom: 0">{{ field.label }}</legend>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for nested_field in field %}
|
||||
{% render_field nested_field template_pack=template_pack renderer=renderer %}
|
||||
{% endfor %}
|
||||
</fieldset>
|
15
rest_framework/templates/rest_framework/horizontal/form.html
Normal file
15
rest_framework/templates/rest_framework/horizontal/form.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% load rest_framework %}
|
||||
<form class="form-horizontal" role="form" action="." method="POST" novalidate>
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
{% if not field.read_only %}
|
||||
{% render_field field style=style %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<!-- form.non_field_errors -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,14 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
|
||||
{% endif %}
|
||||
<div class="col-sm-10">
|
||||
<input name="{{ field.name }}" class="form-control" type="{{ style.input_type }}" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if field.value %}value="{{ field.value }}"{% endif %}>
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
{% load rest_framework %}
|
||||
<fieldset>
|
||||
{% if field.label %}
|
||||
<div class="form-group" style="border-bottom: 1px solid #e5e5e5">
|
||||
<legend class="control-label col-sm-2 {% if style.hide_label %}sr-only{% endif %}" style="border-bottom: 0">{{ field.label }}</legend>
|
||||
</div>
|
||||
{% endif %}
|
||||
<ul>
|
||||
{% for child in field.value %}
|
||||
<li>TODO</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</fieldset>
|
|
@ -0,0 +1,30 @@
|
|||
<div class="form-group">
|
||||
{% if field.label %}
|
||||
<label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
|
||||
{% endif %}
|
||||
<div class="col-sm-10">
|
||||
{% if style.inline %}
|
||||
{% for key, text in field.choices.items %}
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}>
|
||||
{{ text }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for key, text in field.choices.items %}
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}>
|
||||
{{ text }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,18 @@
|
|||
<div class="form-group">
|
||||
{% if field.label %}
|
||||
<label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
|
||||
{% endif %}
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" name="{{ field.name }}">
|
||||
{% for key, text in field.choices.items %}
|
||||
<option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,18 @@
|
|||
<div class="form-group">
|
||||
{% if field.label %}
|
||||
<label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
|
||||
{% endif %}
|
||||
<div class="col-sm-10">
|
||||
<select multiple class="form-control" name="{{ field.name }}">
|
||||
{% for key, text in field.choices.items %}
|
||||
<option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
|
||||
{% endif %}
|
||||
<div class="col-sm-10">
|
||||
<textarea name="{{ field.name }}" class="form-control" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if style.rows %}rows="{{ style.rows }}"{% endif %}>{% if field.value %}{{ field.value }}{% endif %}</textarea>
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="{{ field.name }}" value="true" {% if field.value %}checked{% endif %}>
|
||||
{% if field.label %}{{ field.label }}{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label class="sr-only">{{ field.label }}</label>
|
||||
{% endif %}
|
||||
{% for key, text in field.choices.items %}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="{{ rest_framework/field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}>
|
||||
{{ text }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
|
@ -0,0 +1,4 @@
|
|||
{% load rest_framework %}
|
||||
{% for nested_field in field %}
|
||||
{% render_field nested_field template_pack=template_pack renderer=renderer %}
|
||||
{% endfor %}
|
11
rest_framework/templates/rest_framework/inline/form.html
Normal file
11
rest_framework/templates/rest_framework/inline/form.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
{% load rest_framework %}
|
||||
<form class="form-inline" role="form" action="." method="POST" novalidate>
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
{% if not field.read_only %}
|
||||
{% render_field field style=style %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<!-- form.non_field_errors -->
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
</form>
|
|
@ -0,0 +1,6 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label class="sr-only">{{ field.label }}</label>
|
||||
{% endif %}
|
||||
<input name="{{ field.name }}" class="form-control" type="{{ style.input_type }}" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if field.value %}value="{{ field.value }}"{% endif %}>
|
||||
</div>
|
13
rest_framework/templates/rest_framework/inline/radio.html
Normal file
13
rest_framework/templates/rest_framework/inline/radio.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label class="sr-only">{{ field.label }}</label>
|
||||
{% endif %}
|
||||
{% for key, text in field.choices.items %}
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}>
|
||||
{{ text }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
10
rest_framework/templates/rest_framework/inline/select.html
Normal file
10
rest_framework/templates/rest_framework/inline/select.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label class="sr-only">{{ field.label }}</label>
|
||||
{% endif %}
|
||||
<select class="form-control" name="{{ field.name }}">
|
||||
{% for key, text in field.choices.items %}
|
||||
<option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label class="sr-only">{{ field.label }}</label>
|
||||
{% endif %}
|
||||
<select multiple class="form-control" name="{{ field.name }}">
|
||||
{% for key, text in field.choices.items %}
|
||||
<option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
|
@ -0,0 +1,6 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label class="sr-only">{{ field.label }}</label>
|
||||
{% endif %}
|
||||
<input name="{{ field.name }}" type="text" class="form-control" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if field.value %}value="{{ field.value }}"{% endif %}>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="{{ field.name }}" value="true" {% if value %}checked{% endif %}>
|
||||
{% if field.label %}{{ field.label }}{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
|
@ -0,0 +1,30 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
|
||||
{% endif %}
|
||||
{% if style.inline %}
|
||||
<div>
|
||||
{% for key, text in field.choices.items %}
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}>
|
||||
{{ text }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% for key, text in field.choices.items %}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}>
|
||||
{{ text }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
|
@ -0,0 +1,7 @@
|
|||
{% load rest_framework %}
|
||||
<fieldset>
|
||||
{% if field.label %}<legend {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %}
|
||||
{% for nested_field in field %}
|
||||
{% render_field nested_field template_pack=template_pack renderer=renderer %}
|
||||
{% endfor %}
|
||||
</fieldset>
|
11
rest_framework/templates/rest_framework/vertical/form.html
Normal file
11
rest_framework/templates/rest_framework/vertical/form.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
{% load rest_framework %}
|
||||
<form role="form" action="." method="POST" novalidate>
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
{% if not field.read_only %}
|
||||
{% render_field field style=style %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<!-- form.non_field_errors -->
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
</form>
|
12
rest_framework/templates/rest_framework/vertical/input.html
Normal file
12
rest_framework/templates/rest_framework/vertical/input.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
|
||||
{% endif %}
|
||||
<input name="{{ field.name }}" class="form-control" type="{{ style.input_type }}" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if field.value %}value="{{ field.value }}"{% endif %}>
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
|
@ -0,0 +1,7 @@
|
|||
<fieldset>
|
||||
{% if field.label %}<legend {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %}
|
||||
<!-- {% if field.label %}<legend {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %}
|
||||
{% for field_item in field.value.field_items.values() %}
|
||||
{{ renderer.render_field(field_item, layout=layout) }}
|
||||
{% endfor %} -->
|
||||
</fieldset>
|
30
rest_framework/templates/rest_framework/vertical/radio.html
Normal file
30
rest_framework/templates/rest_framework/vertical/radio.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
|
||||
{% endif %}
|
||||
{% if style.inline %}
|
||||
<div>
|
||||
{% for key, text in field.choices.items %}
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}>
|
||||
{{ text }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% for key, text in field.choices.items %}
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}>
|
||||
{{ text }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
16
rest_framework/templates/rest_framework/vertical/select.html
Normal file
16
rest_framework/templates/rest_framework/vertical/select.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
|
||||
{% endif %}
|
||||
<select class="form-control" name="{{ field.name }}">
|
||||
{% for key, text in field.choices.items %}
|
||||
<option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
|
@ -0,0 +1,16 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
|
||||
{% endif %}
|
||||
<select multiple class="form-control" name="{{ field.name }}">
|
||||
{% for key, text in field.choices.items %}
|
||||
<option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
|
||||
{% endif %}
|
||||
<textarea name="{{ field.name }}" class="form-control" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if style.rows %}rows="{{ style.rows }}"{% endif %}>{% if field.value %}{{ field.value }}{% endif %}</textarea>
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
|
@ -8,6 +8,7 @@ from django.utils.html import escape
|
|||
from django.utils.safestring import SafeData, mark_safe
|
||||
from django.utils.html import smart_urlquote
|
||||
from rest_framework.compat import urlparse, force_text
|
||||
from rest_framework.renderers import HTMLFormRenderer
|
||||
import re
|
||||
|
||||
register = template.Library()
|
||||
|
@ -31,6 +32,13 @@ class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
|
|||
|
||||
# And the template tags themselves...
|
||||
|
||||
@register.simple_tag
|
||||
def render_field(field, style=None):
|
||||
style = style or {}
|
||||
renderer = style.get('renderer', HTMLFormRenderer())
|
||||
return renderer.render_field(field, style)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def optional_login(request):
|
||||
"""
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
Helper classes for parsers.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from django.utils import timezone
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils import six, timezone
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.utils.functional import Promise
|
||||
from rest_framework.compat import force_text
|
||||
from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
|
||||
import datetime
|
||||
import decimal
|
||||
import types
|
||||
|
@ -17,45 +16,47 @@ import json
|
|||
class JSONEncoder(json.JSONEncoder):
|
||||
"""
|
||||
JSONEncoder subclass that knows how to encode date/time/timedelta,
|
||||
decimal types, and generators.
|
||||
decimal types, generators and other basic python objects.
|
||||
"""
|
||||
def default(self, o):
|
||||
def default(self, obj):
|
||||
# For Date Time string spec, see ECMA 262
|
||||
# http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
||||
if isinstance(o, Promise):
|
||||
return force_text(o)
|
||||
elif isinstance(o, datetime.datetime):
|
||||
r = o.isoformat()
|
||||
if o.microsecond:
|
||||
r = r[:23] + r[26:]
|
||||
if r.endswith('+00:00'):
|
||||
r = r[:-6] + 'Z'
|
||||
return r
|
||||
elif isinstance(o, datetime.date):
|
||||
return o.isoformat()
|
||||
elif isinstance(o, datetime.time):
|
||||
if timezone and timezone.is_aware(o):
|
||||
if isinstance(obj, Promise):
|
||||
return force_text(obj)
|
||||
elif isinstance(obj, datetime.datetime):
|
||||
representation = obj.isoformat()
|
||||
if obj.microsecond:
|
||||
representation = representation[:23] + representation[26:]
|
||||
if representation.endswith('+00:00'):
|
||||
representation = representation[:-6] + 'Z'
|
||||
return representation
|
||||
elif isinstance(obj, datetime.date):
|
||||
return obj.isoformat()
|
||||
elif isinstance(obj, datetime.time):
|
||||
if timezone and timezone.is_aware(obj):
|
||||
raise ValueError("JSON can't represent timezone-aware times.")
|
||||
r = o.isoformat()
|
||||
if o.microsecond:
|
||||
r = r[:12]
|
||||
return r
|
||||
elif isinstance(o, datetime.timedelta):
|
||||
return str(o.total_seconds())
|
||||
elif isinstance(o, decimal.Decimal):
|
||||
return str(o)
|
||||
elif isinstance(o, QuerySet):
|
||||
return list(o)
|
||||
elif hasattr(o, 'tolist'):
|
||||
return o.tolist()
|
||||
elif hasattr(o, '__getitem__'):
|
||||
representation = obj.isoformat()
|
||||
if obj.microsecond:
|
||||
representation = representation[:12]
|
||||
return representation
|
||||
elif isinstance(obj, datetime.timedelta):
|
||||
return six.text_type(obj.total_seconds())
|
||||
elif isinstance(obj, decimal.Decimal):
|
||||
# Serializers will coerce decimals to strings by default.
|
||||
return float(obj)
|
||||
elif isinstance(obj, QuerySet):
|
||||
return tuple(obj)
|
||||
elif hasattr(obj, 'tolist'):
|
||||
# Numpy arrays and array scalars.
|
||||
return obj.tolist()
|
||||
elif hasattr(obj, '__getitem__'):
|
||||
try:
|
||||
return dict(o)
|
||||
return dict(obj)
|
||||
except:
|
||||
pass
|
||||
elif hasattr(o, '__iter__'):
|
||||
return [i for i in o]
|
||||
return super(JSONEncoder, self).default(o)
|
||||
elif hasattr(obj, '__iter__'):
|
||||
return tuple(item for item in obj)
|
||||
return super(JSONEncoder, self).default(obj)
|
||||
|
||||
|
||||
try:
|
||||
|
@ -71,7 +72,7 @@ else:
|
|||
than the usual behaviour of sorting the keys.
|
||||
"""
|
||||
def represent_decimal(self, data):
|
||||
return self.represent_scalar('tag:yaml.org,2002:str', str(data))
|
||||
return self.represent_scalar('tag:yaml.org,2002:str', six.text_type(data))
|
||||
|
||||
def represent_mapping(self, tag, mapping, flow_style=None):
|
||||
value = []
|
||||
|
@ -106,14 +107,14 @@ else:
|
|||
SortedDict,
|
||||
yaml.representer.SafeRepresenter.represent_dict
|
||||
)
|
||||
SafeDumper.add_representer(
|
||||
DictWithMetadata,
|
||||
yaml.representer.SafeRepresenter.represent_dict
|
||||
)
|
||||
SafeDumper.add_representer(
|
||||
SortedDictWithMetadata,
|
||||
yaml.representer.SafeRepresenter.represent_dict
|
||||
)
|
||||
# SafeDumper.add_representer(
|
||||
# DictWithMetadata,
|
||||
# yaml.representer.SafeRepresenter.represent_dict
|
||||
# )
|
||||
# SafeDumper.add_representer(
|
||||
# SortedDictWithMetadata,
|
||||
# yaml.representer.SafeRepresenter.represent_dict
|
||||
# )
|
||||
SafeDumper.add_representer(
|
||||
types.GeneratorType,
|
||||
yaml.representer.SafeRepresenter.represent_list
|
||||
|
|
239
rest_framework/utils/field_mapping.py
Normal file
239
rest_framework/utils/field_mapping.py
Normal file
|
@ -0,0 +1,239 @@
|
|||
"""
|
||||
Helper functions for mapping model fields to a dictionary of default
|
||||
keyword arguments that should be used for their equivelent serializer fields.
|
||||
"""
|
||||
from django.core import validators
|
||||
from django.db import models
|
||||
from django.utils.text import capfirst
|
||||
from rest_framework.compat import clean_manytomany_helptext
|
||||
from rest_framework.validators import UniqueValidator
|
||||
import inspect
|
||||
|
||||
|
||||
class ClassLookupDict(object):
|
||||
"""
|
||||
Takes a dictionary with classes as keys.
|
||||
Lookups against this object will traverses the object's inheritance
|
||||
hierarchy in method resolution order, and returns the first matching value
|
||||
from the dictionary or raises a KeyError if nothing matches.
|
||||
"""
|
||||
def __init__(self, mapping):
|
||||
self.mapping = mapping
|
||||
|
||||
def __getitem__(self, key):
|
||||
if hasattr(key, '_proxy_class'):
|
||||
# Deal with proxy classes. Ie. BoundField behaves as if it
|
||||
# is a Field instance when using ClassLookupDict.
|
||||
base_class = key._proxy_class
|
||||
else:
|
||||
base_class = key.__class__
|
||||
|
||||
for cls in inspect.getmro(base_class):
|
||||
if cls in self.mapping:
|
||||
return self.mapping[cls]
|
||||
raise KeyError('Class %s not found in lookup.', cls.__name__)
|
||||
|
||||
|
||||
def needs_label(model_field, field_name):
|
||||
"""
|
||||
Returns `True` if the label based on the model's verbose name
|
||||
is not equal to the default label it would have based on it's field name.
|
||||
"""
|
||||
default_label = field_name.replace('_', ' ').capitalize()
|
||||
return capfirst(model_field.verbose_name) != default_label
|
||||
|
||||
|
||||
def get_detail_view_name(model):
|
||||
"""
|
||||
Given a model class, return the view name to use for URL relationships
|
||||
that refer to instances of the model.
|
||||
"""
|
||||
return '%(model_name)s-detail' % {
|
||||
'app_label': model._meta.app_label,
|
||||
'model_name': model._meta.object_name.lower()
|
||||
}
|
||||
|
||||
|
||||
def get_field_kwargs(field_name, model_field):
|
||||
"""
|
||||
Creates a default instance of a basic non-relational field.
|
||||
"""
|
||||
kwargs = {}
|
||||
validator_kwarg = model_field.validators
|
||||
|
||||
# The following will only be used by ModelField classes.
|
||||
# Gets removed for everything else.
|
||||
kwargs['model_field'] = model_field
|
||||
|
||||
if model_field.verbose_name and needs_label(model_field, field_name):
|
||||
kwargs['label'] = capfirst(model_field.verbose_name)
|
||||
|
||||
if model_field.help_text:
|
||||
kwargs['help_text'] = model_field.help_text
|
||||
|
||||
max_digits = getattr(model_field, 'max_digits', None)
|
||||
if max_digits is not None:
|
||||
kwargs['max_digits'] = max_digits
|
||||
|
||||
decimal_places = getattr(model_field, 'decimal_places', None)
|
||||
if decimal_places is not None:
|
||||
kwargs['decimal_places'] = decimal_places
|
||||
|
||||
if isinstance(model_field, models.TextField):
|
||||
kwargs['style'] = {'type': 'textarea'}
|
||||
|
||||
if isinstance(model_field, models.AutoField) or not model_field.editable:
|
||||
# If this field is read-only, then return early.
|
||||
# Further keyword arguments are not valid.
|
||||
kwargs['read_only'] = True
|
||||
return kwargs
|
||||
|
||||
if model_field.has_default():
|
||||
kwargs['required'] = False
|
||||
|
||||
if model_field.flatchoices:
|
||||
# If this model field contains choices, then return early.
|
||||
# Further keyword arguments are not valid.
|
||||
kwargs['choices'] = model_field.flatchoices
|
||||
return kwargs
|
||||
|
||||
if model_field.null and not isinstance(model_field, models.NullBooleanField):
|
||||
kwargs['allow_null'] = True
|
||||
|
||||
if model_field.blank:
|
||||
kwargs['allow_blank'] = True
|
||||
|
||||
# Ensure that max_length is passed explicitly as a keyword arg,
|
||||
# rather than as a validator.
|
||||
max_length = getattr(model_field, 'max_length', None)
|
||||
if max_length is not None:
|
||||
kwargs['max_length'] = max_length
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if not isinstance(validator, validators.MaxLengthValidator)
|
||||
]
|
||||
|
||||
# Ensure that min_length is passed explicitly as a keyword arg,
|
||||
# rather than as a validator.
|
||||
min_length = next((
|
||||
validator.limit_value for validator in validator_kwarg
|
||||
if isinstance(validator, validators.MinLengthValidator)
|
||||
), None)
|
||||
if min_length is not None:
|
||||
kwargs['min_length'] = min_length
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if not isinstance(validator, validators.MinLengthValidator)
|
||||
]
|
||||
|
||||
# Ensure that max_value is passed explicitly as a keyword arg,
|
||||
# rather than as a validator.
|
||||
max_value = next((
|
||||
validator.limit_value for validator in validator_kwarg
|
||||
if isinstance(validator, validators.MaxValueValidator)
|
||||
), None)
|
||||
if max_value is not None:
|
||||
kwargs['max_value'] = max_value
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if not isinstance(validator, validators.MaxValueValidator)
|
||||
]
|
||||
|
||||
# Ensure that max_value is passed explicitly as a keyword arg,
|
||||
# rather than as a validator.
|
||||
min_value = next((
|
||||
validator.limit_value for validator in validator_kwarg
|
||||
if isinstance(validator, validators.MinValueValidator)
|
||||
), None)
|
||||
if min_value is not None:
|
||||
kwargs['min_value'] = min_value
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if not isinstance(validator, validators.MinValueValidator)
|
||||
]
|
||||
|
||||
# URLField does not need to include the URLValidator argument,
|
||||
# as it is explicitly added in.
|
||||
if isinstance(model_field, models.URLField):
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if not isinstance(validator, validators.URLValidator)
|
||||
]
|
||||
|
||||
# EmailField does not need to include the validate_email argument,
|
||||
# as it is explicitly added in.
|
||||
if isinstance(model_field, models.EmailField):
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if validator is not validators.validate_email
|
||||
]
|
||||
|
||||
# SlugField do not need to include the 'validate_slug' argument,
|
||||
if isinstance(model_field, models.SlugField):
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if validator is not validators.validate_slug
|
||||
]
|
||||
|
||||
if getattr(model_field, 'unique', False):
|
||||
validator = UniqueValidator(queryset=model_field.model._default_manager)
|
||||
validator_kwarg.append(validator)
|
||||
|
||||
if validator_kwarg:
|
||||
kwargs['validators'] = validator_kwarg
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
def get_relation_kwargs(field_name, relation_info):
|
||||
"""
|
||||
Creates a default instance of a flat relational field.
|
||||
"""
|
||||
model_field, related_model, to_many, has_through_model = relation_info
|
||||
kwargs = {
|
||||
'queryset': related_model._default_manager,
|
||||
'view_name': get_detail_view_name(related_model)
|
||||
}
|
||||
|
||||
if to_many:
|
||||
kwargs['many'] = True
|
||||
|
||||
if has_through_model:
|
||||
kwargs['read_only'] = True
|
||||
kwargs.pop('queryset', None)
|
||||
|
||||
if model_field:
|
||||
if model_field.verbose_name and needs_label(model_field, field_name):
|
||||
kwargs['label'] = capfirst(model_field.verbose_name)
|
||||
help_text = clean_manytomany_helptext(model_field.help_text)
|
||||
if help_text:
|
||||
kwargs['help_text'] = help_text
|
||||
if not model_field.editable:
|
||||
kwargs['read_only'] = True
|
||||
kwargs.pop('queryset', None)
|
||||
if kwargs.get('read_only', False):
|
||||
# If this field is read-only, then return early.
|
||||
# No further keyword arguments are valid.
|
||||
return kwargs
|
||||
if model_field.has_default():
|
||||
kwargs['required'] = False
|
||||
if model_field.null:
|
||||
kwargs['allow_null'] = True
|
||||
if getattr(model_field, 'unique', False):
|
||||
validator = UniqueValidator(queryset=model_field.model._default_manager)
|
||||
kwargs['validators'] = [validator]
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
def get_nested_relation_kwargs(relation_info):
|
||||
kwargs = {'read_only': True}
|
||||
if relation_info.to_many:
|
||||
kwargs['many'] = True
|
||||
return kwargs
|
||||
|
||||
|
||||
def get_url_kwargs(model_field):
|
||||
return {
|
||||
'view_name': get_detail_view_name(model_field)
|
||||
}
|
88
rest_framework/utils/html.py
Normal file
88
rest_framework/utils/html.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
"""
|
||||
Helpers for dealing with HTML input.
|
||||
"""
|
||||
import re
|
||||
|
||||
|
||||
def is_html_input(dictionary):
|
||||
# MultiDict type datastructures are used to represent HTML form input,
|
||||
# which may have more than one value for each key.
|
||||
return hasattr(dictionary, 'getlist')
|
||||
|
||||
|
||||
def parse_html_list(dictionary, prefix=''):
|
||||
"""
|
||||
Used to suport list values in HTML forms.
|
||||
Supports lists of primitives and/or dictionaries.
|
||||
|
||||
* List of primitives.
|
||||
|
||||
{
|
||||
'[0]': 'abc',
|
||||
'[1]': 'def',
|
||||
'[2]': 'hij'
|
||||
}
|
||||
-->
|
||||
[
|
||||
'abc',
|
||||
'def',
|
||||
'hij'
|
||||
]
|
||||
|
||||
* List of dictionaries.
|
||||
|
||||
{
|
||||
'[0]foo': 'abc',
|
||||
'[0]bar': 'def',
|
||||
'[1]foo': 'hij',
|
||||
'[2]bar': 'klm',
|
||||
}
|
||||
-->
|
||||
[
|
||||
{'foo': 'abc', 'bar': 'def'},
|
||||
{'foo': 'hij', 'bar': 'klm'}
|
||||
]
|
||||
"""
|
||||
Dict = type(dictionary)
|
||||
ret = {}
|
||||
regex = re.compile(r'^%s\[([0-9]+)\](.*)$' % re.escape(prefix))
|
||||
for field, value in dictionary.items():
|
||||
match = regex.match(field)
|
||||
if not match:
|
||||
continue
|
||||
index, key = match.groups()
|
||||
index = int(index)
|
||||
if not key:
|
||||
ret[index] = value
|
||||
elif isinstance(ret.get(index), dict):
|
||||
ret[index][key] = value
|
||||
else:
|
||||
ret[index] = Dict({key: value})
|
||||
return [ret[item] for item in sorted(ret.keys())]
|
||||
|
||||
|
||||
def parse_html_dict(dictionary, prefix):
|
||||
"""
|
||||
Used to support dictionary values in HTML forms.
|
||||
|
||||
{
|
||||
'profile.username': 'example',
|
||||
'profile.email': 'example@example.com',
|
||||
}
|
||||
-->
|
||||
{
|
||||
'profile': {
|
||||
'username': 'example,
|
||||
'email': 'example@example.com'
|
||||
}
|
||||
}
|
||||
"""
|
||||
ret = {}
|
||||
regex = re.compile(r'^%s\.(.+)$' % re.escape(prefix))
|
||||
for field, value in dictionary.items():
|
||||
match = regex.match(field)
|
||||
if not match:
|
||||
continue
|
||||
key = match.groups()[0]
|
||||
ret[key] = value
|
||||
return ret
|
47
rest_framework/utils/humanize_datetime.py
Normal file
47
rest_framework/utils/humanize_datetime.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
"""
|
||||
Helper functions that convert strftime formats into more readable representations.
|
||||
"""
|
||||
from rest_framework import ISO_8601
|
||||
|
||||
|
||||
def datetime_formats(formats):
|
||||
format = ', '.join(formats).replace(
|
||||
ISO_8601,
|
||||
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'
|
||||
)
|
||||
return humanize_strptime(format)
|
||||
|
||||
|
||||
def date_formats(formats):
|
||||
format = ', '.join(formats).replace(ISO_8601, 'YYYY[-MM[-DD]]')
|
||||
return humanize_strptime(format)
|
||||
|
||||
|
||||
def time_formats(formats):
|
||||
format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]')
|
||||
return humanize_strptime(format)
|
||||
|
||||
|
||||
def humanize_strptime(format_string):
|
||||
# Note that we're missing some of the locale specific mappings that
|
||||
# don't really make sense.
|
||||
mapping = {
|
||||
"%Y": "YYYY",
|
||||
"%y": "YY",
|
||||
"%m": "MM",
|
||||
"%b": "[Jan-Dec]",
|
||||
"%B": "[January-December]",
|
||||
"%d": "DD",
|
||||
"%H": "hh",
|
||||
"%I": "hh", # Requires '%p' to differentiate from '%H'.
|
||||
"%M": "mm",
|
||||
"%S": "ss",
|
||||
"%f": "uuuuuu",
|
||||
"%a": "[Mon-Sun]",
|
||||
"%A": "[Monday-Sunday]",
|
||||
"%p": "[AM|PM]",
|
||||
"%z": "[+HHMM|-HHMM]"
|
||||
}
|
||||
for key, val in mapping.items():
|
||||
format_string = format_string.replace(key, val)
|
||||
return format_string
|
129
rest_framework/utils/model_meta.py
Normal file
129
rest_framework/utils/model_meta.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
"""
|
||||
Helper function for returning the field information that is associated
|
||||
with a model class. This includes returning all the forward and reverse
|
||||
relationships and their associated metadata.
|
||||
|
||||
Usage: `get_field_info(model)` returns a `FieldInfo` instance.
|
||||
"""
|
||||
from collections import namedtuple
|
||||
from django.db import models
|
||||
from django.utils import six
|
||||
from django.utils.datastructures import SortedDict
|
||||
import inspect
|
||||
|
||||
|
||||
FieldInfo = namedtuple('FieldResult', [
|
||||
'pk', # Model field instance
|
||||
'fields', # Dict of field name -> model field instance
|
||||
'forward_relations', # Dict of field name -> RelationInfo
|
||||
'reverse_relations', # Dict of field name -> RelationInfo
|
||||
'fields_and_pk', # Shortcut for 'pk' + 'fields'
|
||||
'relations' # Shortcut for 'forward_relations' + 'reverse_relations'
|
||||
])
|
||||
|
||||
RelationInfo = namedtuple('RelationInfo', [
|
||||
'model_field',
|
||||
'related',
|
||||
'to_many',
|
||||
'has_through_model'
|
||||
])
|
||||
|
||||
|
||||
def _resolve_model(obj):
|
||||
"""
|
||||
Resolve supplied `obj` to a Django model class.
|
||||
|
||||
`obj` must be a Django model class itself, or a string
|
||||
representation of one. Useful in situtations like GH #1225 where
|
||||
Django may not have resolved a string-based reference to a model in
|
||||
another model's foreign key definition.
|
||||
|
||||
String representations should have the format:
|
||||
'appname.ModelName'
|
||||
"""
|
||||
if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
|
||||
app_name, model_name = obj.split('.')
|
||||
return models.get_model(app_name, model_name)
|
||||
elif inspect.isclass(obj) and issubclass(obj, models.Model):
|
||||
return obj
|
||||
raise ValueError("{0} is not a Django model".format(obj))
|
||||
|
||||
|
||||
def get_field_info(model):
|
||||
"""
|
||||
Given a model class, returns a `FieldInfo` instance containing metadata
|
||||
about the various field types on the model.
|
||||
"""
|
||||
opts = model._meta.concrete_model._meta
|
||||
|
||||
# Deal with the primary key.
|
||||
pk = opts.pk
|
||||
while pk.rel and pk.rel.parent_link:
|
||||
# If model is a child via multitable inheritance, use parent's pk.
|
||||
pk = pk.rel.to._meta.pk
|
||||
|
||||
# Deal with regular fields.
|
||||
fields = SortedDict()
|
||||
for field in [field for field in opts.fields if field.serialize and not field.rel]:
|
||||
fields[field.name] = field
|
||||
|
||||
# Deal with forward relationships.
|
||||
forward_relations = SortedDict()
|
||||
for field in [field for field in opts.fields if field.serialize and field.rel]:
|
||||
forward_relations[field.name] = RelationInfo(
|
||||
model_field=field,
|
||||
related=_resolve_model(field.rel.to),
|
||||
to_many=False,
|
||||
has_through_model=False
|
||||
)
|
||||
|
||||
# Deal with forward many-to-many relationships.
|
||||
for field in [field for field in opts.many_to_many if field.serialize]:
|
||||
forward_relations[field.name] = RelationInfo(
|
||||
model_field=field,
|
||||
related=_resolve_model(field.rel.to),
|
||||
to_many=True,
|
||||
has_through_model=(
|
||||
not field.rel.through._meta.auto_created
|
||||
)
|
||||
)
|
||||
|
||||
# Deal with reverse relationships.
|
||||
reverse_relations = SortedDict()
|
||||
for relation in opts.get_all_related_objects():
|
||||
accessor_name = relation.get_accessor_name()
|
||||
reverse_relations[accessor_name] = RelationInfo(
|
||||
model_field=None,
|
||||
related=relation.model,
|
||||
to_many=relation.field.rel.multiple,
|
||||
has_through_model=False
|
||||
)
|
||||
|
||||
# Deal with reverse many-to-many relationships.
|
||||
for relation in opts.get_all_related_many_to_many_objects():
|
||||
accessor_name = relation.get_accessor_name()
|
||||
reverse_relations[accessor_name] = RelationInfo(
|
||||
model_field=None,
|
||||
related=relation.model,
|
||||
to_many=True,
|
||||
has_through_model=(
|
||||
(getattr(relation.field.rel, 'through', None) is not None)
|
||||
and not relation.field.rel.through._meta.auto_created
|
||||
)
|
||||
)
|
||||
|
||||
# Shortcut that merges both regular fields and the pk,
|
||||
# for simplifying regular field lookup.
|
||||
fields_and_pk = SortedDict()
|
||||
fields_and_pk['pk'] = pk
|
||||
fields_and_pk[pk.name] = pk
|
||||
fields_and_pk.update(fields)
|
||||
|
||||
# Shortcut that merges both forward and reverse relationships
|
||||
|
||||
relations = SortedDict(
|
||||
list(forward_relations.items()) +
|
||||
list(reverse_relations.items())
|
||||
)
|
||||
|
||||
return FieldInfo(pk, fields, forward_relations, reverse_relations, fields_and_pk, relations)
|
97
rest_framework/utils/representation.py
Normal file
97
rest_framework/utils/representation.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
"""
|
||||
Helper functions for creating user-friendly representations
|
||||
of serializer classes and serializer fields.
|
||||
"""
|
||||
from django.db import models
|
||||
from django.utils.functional import Promise
|
||||
from rest_framework.compat import force_text
|
||||
import re
|
||||
|
||||
|
||||
def manager_repr(value):
|
||||
model = value.model
|
||||
opts = model._meta
|
||||
for _, name, manager in opts.concrete_managers + opts.abstract_managers:
|
||||
if manager == value:
|
||||
return '%s.%s.all()' % (model._meta.object_name, name)
|
||||
return repr(value)
|
||||
|
||||
|
||||
def smart_repr(value):
|
||||
if isinstance(value, models.Manager):
|
||||
return manager_repr(value)
|
||||
|
||||
if isinstance(value, Promise) and value._delegate_text:
|
||||
value = force_text(value)
|
||||
|
||||
value = repr(value)
|
||||
|
||||
# Representations like u'help text'
|
||||
# should simply be presented as 'help text'
|
||||
if value.startswith("u'") and value.endswith("'"):
|
||||
return value[1:]
|
||||
|
||||
# Representations like
|
||||
# <django.core.validators.RegexValidator object at 0x1047af050>
|
||||
# Should be presented as
|
||||
# <django.core.validators.RegexValidator object>
|
||||
value = re.sub(' at 0x[0-9a-f]{4,32}>', '>', value)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def field_repr(field, force_many=False):
|
||||
kwargs = field._kwargs
|
||||
if force_many:
|
||||
kwargs = kwargs.copy()
|
||||
kwargs['many'] = True
|
||||
kwargs.pop('child', None)
|
||||
|
||||
arg_string = ', '.join([smart_repr(val) for val in field._args])
|
||||
kwarg_string = ', '.join([
|
||||
'%s=%s' % (key, smart_repr(val))
|
||||
for key, val in sorted(kwargs.items())
|
||||
])
|
||||
if arg_string and kwarg_string:
|
||||
arg_string += ', '
|
||||
|
||||
if force_many:
|
||||
class_name = force_many.__class__.__name__
|
||||
else:
|
||||
class_name = field.__class__.__name__
|
||||
|
||||
return "%s(%s%s)" % (class_name, arg_string, kwarg_string)
|
||||
|
||||
|
||||
def serializer_repr(serializer, indent, force_many=None):
|
||||
ret = field_repr(serializer, force_many) + ':'
|
||||
indent_str = ' ' * indent
|
||||
|
||||
if force_many:
|
||||
fields = force_many.fields
|
||||
else:
|
||||
fields = serializer.fields
|
||||
|
||||
for field_name, field in fields.items():
|
||||
ret += '\n' + indent_str + field_name + ' = '
|
||||
if hasattr(field, 'fields'):
|
||||
ret += serializer_repr(field, indent + 1)
|
||||
elif hasattr(field, 'child'):
|
||||
ret += list_repr(field, indent + 1)
|
||||
elif hasattr(field, 'child_relation'):
|
||||
ret += field_repr(field.child_relation, force_many=field.child_relation)
|
||||
else:
|
||||
ret += field_repr(field)
|
||||
|
||||
if serializer.validators:
|
||||
ret += '\n' + indent_str + 'class Meta:'
|
||||
ret += '\n' + indent_str + ' validators = ' + smart_repr(serializer.validators)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def list_repr(serializer, indent):
|
||||
child = serializer.child
|
||||
if hasattr(child, 'fields'):
|
||||
return serializer_repr(serializer, indent, force_many=child)
|
||||
return field_repr(serializer)
|
165
rest_framework/validators.py
Normal file
165
rest_framework/validators.py
Normal file
|
@ -0,0 +1,165 @@
|
|||
"""
|
||||
We perform uniqueness checks explicitly on the serializer class, rather
|
||||
the using Django's `.full_clean()`.
|
||||
|
||||
This gives us better separation of concerns, allows us to use single-step
|
||||
object creation, and makes it possible to switch between using the implicit
|
||||
`ModelSerializer` class and an equivelent explicit `Serializer` class.
|
||||
"""
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.utils.representation import smart_repr
|
||||
|
||||
|
||||
class UniqueValidator:
|
||||
"""
|
||||
Validator that corresponds to `unique=True` on a model field.
|
||||
|
||||
Should be applied to an individual field on the serializer.
|
||||
"""
|
||||
message = _('This field must be unique.')
|
||||
|
||||
def __init__(self, queryset, message=None):
|
||||
self.queryset = queryset
|
||||
self.serializer_field = None
|
||||
self.message = message or self.message
|
||||
|
||||
def set_context(self, serializer_field):
|
||||
# Determine the underlying model field name. This may not be the
|
||||
# same as the serializer field name if `source=<>` is set.
|
||||
self.field_name = serializer_field.source_attrs[0]
|
||||
# Determine the existing instance, if this is an update operation.
|
||||
self.instance = getattr(serializer_field.parent, 'instance', None)
|
||||
|
||||
def __call__(self, value):
|
||||
# Ensure uniqueness.
|
||||
filter_kwargs = {self.field_name: value}
|
||||
queryset = self.queryset.filter(**filter_kwargs)
|
||||
if self.instance is not None:
|
||||
queryset = queryset.exclude(pk=self.instance.pk)
|
||||
if queryset.exists():
|
||||
raise ValidationError(self.message)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s(queryset=%s)>' % (
|
||||
self.__class__.__name__,
|
||||
smart_repr(self.queryset)
|
||||
)
|
||||
|
||||
|
||||
class UniqueTogetherValidator:
|
||||
"""
|
||||
Validator that corresponds to `unique_together = (...)` on a model class.
|
||||
|
||||
Should be applied to the serializer class, not to an individual field.
|
||||
"""
|
||||
message = _('The fields {field_names} must make a unique set.')
|
||||
|
||||
def __init__(self, queryset, fields, message=None):
|
||||
self.queryset = queryset
|
||||
self.fields = fields
|
||||
self.serializer_field = None
|
||||
self.message = message or self.message
|
||||
|
||||
def set_context(self, serializer):
|
||||
# Determine the existing instance, if this is an update operation.
|
||||
self.instance = getattr(serializer, 'instance', None)
|
||||
|
||||
def __call__(self, attrs):
|
||||
# Ensure uniqueness.
|
||||
filter_kwargs = dict([
|
||||
(field_name, attrs[field_name]) for field_name in self.fields
|
||||
])
|
||||
queryset = self.queryset.filter(**filter_kwargs)
|
||||
if self.instance is not None:
|
||||
queryset = queryset.exclude(pk=self.instance.pk)
|
||||
if queryset.exists():
|
||||
field_names = ', '.join(self.fields)
|
||||
raise ValidationError(self.message.format(field_names=field_names))
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s(queryset=%s, fields=%s)>' % (
|
||||
self.__class__.__name__,
|
||||
smart_repr(self.queryset),
|
||||
smart_repr(self.fields)
|
||||
)
|
||||
|
||||
|
||||
class BaseUniqueForValidator:
|
||||
message = None
|
||||
|
||||
def __init__(self, queryset, field, date_field, message=None):
|
||||
self.queryset = queryset
|
||||
self.field = field
|
||||
self.date_field = date_field
|
||||
self.message = message or self.message
|
||||
|
||||
def set_context(self, serializer):
|
||||
# Determine the underlying model field names. These may not be the
|
||||
# same as the serializer field names if `source=<>` is set.
|
||||
self.field_name = serializer.fields[self.field].source_attrs[0]
|
||||
self.date_field_name = serializer.fields[self.date_field].source_attrs[0]
|
||||
# Determine the existing instance, if this is an update operation.
|
||||
self.instance = getattr(serializer, 'instance', None)
|
||||
|
||||
def get_filter_kwargs(self, attrs):
|
||||
raise NotImplementedError('`get_filter_kwargs` must be implemented.')
|
||||
|
||||
def __call__(self, attrs):
|
||||
filter_kwargs = self.get_filter_kwargs(attrs)
|
||||
|
||||
queryset = self.queryset.filter(**filter_kwargs)
|
||||
if self.instance is not None:
|
||||
queryset = queryset.exclude(pk=self.instance.pk)
|
||||
if queryset.exists():
|
||||
message = self.message.format(date_field=self.date_field)
|
||||
raise ValidationError({self.field: message})
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s(queryset=%s, field=%s, date_field=%s)>' % (
|
||||
self.__class__.__name__,
|
||||
smart_repr(self.queryset),
|
||||
smart_repr(self.field),
|
||||
smart_repr(self.date_field)
|
||||
)
|
||||
|
||||
|
||||
class UniqueForDateValidator(BaseUniqueForValidator):
|
||||
message = _('This field must be unique for the "{date_field}" date.')
|
||||
|
||||
def get_filter_kwargs(self, attrs):
|
||||
value = attrs[self.field]
|
||||
date = attrs[self.date_field]
|
||||
|
||||
filter_kwargs = {}
|
||||
filter_kwargs[self.field_name] = value
|
||||
filter_kwargs['%s__day' % self.date_field_name] = date.day
|
||||
filter_kwargs['%s__month' % self.date_field_name] = date.month
|
||||
filter_kwargs['%s__year' % self.date_field_name] = date.year
|
||||
return filter_kwargs
|
||||
|
||||
|
||||
class UniqueForMonthValidator(BaseUniqueForValidator):
|
||||
message = _('This field must be unique for the "{date_field}" month.')
|
||||
|
||||
def get_filter_kwargs(self, attrs):
|
||||
value = attrs[self.field]
|
||||
date = attrs[self.date_field]
|
||||
|
||||
filter_kwargs = {}
|
||||
filter_kwargs[self.field_name] = value
|
||||
filter_kwargs['%s__month' % self.date_field_name] = date.month
|
||||
return filter_kwargs
|
||||
|
||||
|
||||
class UniqueForYearValidator(BaseUniqueForValidator):
|
||||
message = _('This field must be unique for the "{date_field}" year.')
|
||||
|
||||
def get_filter_kwargs(self, attrs):
|
||||
value = attrs[self.field]
|
||||
date = attrs[self.date_field]
|
||||
|
||||
filter_kwargs = {}
|
||||
filter_kwargs[self.field_name] = value
|
||||
filter_kwargs['%s__year' % self.date_field_name] = date.year
|
||||
return filter_kwargs
|
|
@ -5,7 +5,6 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from rest_framework import status, exceptions
|
||||
from rest_framework.compat import smart_text, HttpResponseBase, View
|
||||
|
@ -51,7 +50,8 @@ def exception_handler(exc):
|
|||
Returns the response that should be used for any given exception.
|
||||
|
||||
By default we handle the REST framework `APIException`, and also
|
||||
Django's builtin `Http404` and `PermissionDenied` exceptions.
|
||||
Django's built-in `ValidationError`, `Http404` and `PermissionDenied`
|
||||
exceptions.
|
||||
|
||||
Any unhandled exceptions may return `None`, which will cause a 500 error
|
||||
to be raised.
|
||||
|
@ -61,20 +61,22 @@ def exception_handler(exc):
|
|||
if getattr(exc, 'auth_header', None):
|
||||
headers['WWW-Authenticate'] = exc.auth_header
|
||||
if getattr(exc, 'wait', None):
|
||||
headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
|
||||
headers['Retry-After'] = '%d' % exc.wait
|
||||
|
||||
return Response({'detail': exc.detail},
|
||||
status=exc.status_code,
|
||||
headers=headers)
|
||||
if isinstance(exc.detail, (list, dict)):
|
||||
data = exc.detail
|
||||
else:
|
||||
data = {'detail': exc.detail}
|
||||
|
||||
return Response(data, status=exc.status_code, headers=headers)
|
||||
|
||||
elif isinstance(exc, Http404):
|
||||
return Response({'detail': 'Not found'},
|
||||
status=status.HTTP_404_NOT_FOUND)
|
||||
data = {'detail': 'Not found'}
|
||||
return Response(data, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
elif isinstance(exc, PermissionDenied):
|
||||
return Response({'detail': 'Permission denied'},
|
||||
status=status.HTTP_403_FORBIDDEN)
|
||||
data = {'detail': 'Permission denied'}
|
||||
return Response(data, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Note: Unhandled exceptions will raise a 500 error.
|
||||
return None
|
||||
|
@ -89,8 +91,9 @@ class APIView(View):
|
|||
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
|
||||
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
|
||||
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
|
||||
metadata_class = api_settings.DEFAULT_METADATA_CLASS
|
||||
|
||||
# Allow dependancy injection of other settings to make testing easier.
|
||||
# Allow dependency injection of other settings to make testing easier.
|
||||
settings = api_settings
|
||||
|
||||
@classmethod
|
||||
|
@ -408,22 +411,8 @@ class APIView(View):
|
|||
def options(self, request, *args, **kwargs):
|
||||
"""
|
||||
Handler method for HTTP 'OPTIONS' request.
|
||||
We may as well implement this as Django will otherwise provide
|
||||
a less useful default implementation.
|
||||
"""
|
||||
return Response(self.metadata(request), status=status.HTTP_200_OK)
|
||||
|
||||
def metadata(self, request):
|
||||
"""
|
||||
Return a dictionary of metadata about the view.
|
||||
Used to return responses for OPTIONS requests.
|
||||
"""
|
||||
# By default we can't provide any form-like information, however the
|
||||
# generic views override this implementation and add additional
|
||||
# information for POST and PUT methods, based on the serializer.
|
||||
ret = SortedDict()
|
||||
ret['name'] = self.get_view_name()
|
||||
ret['description'] = self.get_view_description()
|
||||
ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
|
||||
ret['parses'] = [parser.media_type for parser in self.parser_classes]
|
||||
return ret
|
||||
if self.metadata_class is None:
|
||||
return self.http_method_not_allowed(request, *args, **kwargs)
|
||||
data = self.metadata_class().determine_metadata(request, self)
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
def foobar():
|
||||
|
@ -178,9 +177,3 @@ class NullableOneToOneSource(RESTFrameworkModel):
|
|||
name = models.CharField(max_length=100)
|
||||
target = models.OneToOneField(OneToOneTarget, null=True, blank=True,
|
||||
related_name='nullable_source')
|
||||
|
||||
|
||||
# Serializer used to test BasicModel
|
||||
class BasicModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BasicModel
|
||||
|
|
33
tests/put_as_create_workspace.txt
Normal file
33
tests/put_as_create_workspace.txt
Normal file
|
@ -0,0 +1,33 @@
|
|||
# From test_validation...
|
||||
|
||||
class TestPreSaveValidationExclusions(TestCase):
|
||||
def test_pre_save_validation_exclusions(self):
|
||||
"""
|
||||
Somewhat weird test case to ensure that we don't perform model
|
||||
validation on read only fields.
|
||||
"""
|
||||
obj = ValidationModel.objects.create(blank_validated_field='')
|
||||
request = factory.put('/', {}, format='json')
|
||||
view = UpdateValidationModel().as_view()
|
||||
response = view(request, pk=obj.pk).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
||||
# From test_permissions...
|
||||
|
||||
class ModelPermissionsIntegrationTests(TestCase):
|
||||
def setUp(...):
|
||||
...
|
||||
|
||||
def test_has_put_as_create_permissions(self):
|
||||
# User only has update permissions - should be able to update an entity.
|
||||
request = factory.put('/1', {'text': 'foobar'}, format='json',
|
||||
HTTP_AUTHORIZATION=self.updateonly_credentials)
|
||||
response = instance_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# But if PUTing to a new entity, permission should be denied.
|
||||
request = factory.put('/2', {'text': 'foobar'}, format='json',
|
||||
HTTP_AUTHORIZATION=self.updateonly_credentials)
|
||||
response = instance_view(request, pk='2')
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
@ -1,7 +0,0 @@
|
|||
from rest_framework import serializers
|
||||
from tests.models import NullableForeignKeySource
|
||||
|
||||
|
||||
class NullableFKSourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = NullableForeignKeySource
|
69
tests/test_bound_fields.py
Normal file
69
tests/test_bound_fields.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
|
||||
class TestSimpleBoundField:
|
||||
def test_empty_bound_field(self):
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
text = serializers.CharField(max_length=100)
|
||||
amount = serializers.IntegerField()
|
||||
|
||||
serializer = ExampleSerializer()
|
||||
|
||||
assert serializer['text'].value == ''
|
||||
assert serializer['text'].errors is None
|
||||
assert serializer['text'].name == 'text'
|
||||
assert serializer['amount'].value is None
|
||||
assert serializer['amount'].errors is None
|
||||
assert serializer['amount'].name == 'amount'
|
||||
|
||||
def test_populated_bound_field(self):
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
text = serializers.CharField(max_length=100)
|
||||
amount = serializers.IntegerField()
|
||||
|
||||
serializer = ExampleSerializer(data={'text': 'abc', 'amount': 123})
|
||||
|
||||
assert serializer['text'].value == 'abc'
|
||||
assert serializer['text'].errors is None
|
||||
assert serializer['text'].name == 'text'
|
||||
assert serializer['amount'].value is 123
|
||||
assert serializer['amount'].errors is None
|
||||
assert serializer['amount'].name == 'amount'
|
||||
|
||||
def test_error_bound_field(self):
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
text = serializers.CharField(max_length=100)
|
||||
amount = serializers.IntegerField()
|
||||
|
||||
serializer = ExampleSerializer(data={'text': 'x' * 1000, 'amount': 123})
|
||||
serializer.is_valid()
|
||||
|
||||
assert serializer['text'].value == 'x' * 1000
|
||||
assert serializer['text'].errors == ['Ensure this field has no more than 100 characters.']
|
||||
assert serializer['text'].name == 'text'
|
||||
assert serializer['amount'].value is 123
|
||||
assert serializer['amount'].errors is None
|
||||
assert serializer['amount'].name == 'amount'
|
||||
|
||||
|
||||
class TestNestedBoundField:
|
||||
def test_nested_empty_bound_field(self):
|
||||
class Nested(serializers.Serializer):
|
||||
more_text = serializers.CharField(max_length=100)
|
||||
amount = serializers.IntegerField()
|
||||
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
text = serializers.CharField(max_length=100)
|
||||
nested = Nested()
|
||||
|
||||
serializer = ExampleSerializer()
|
||||
|
||||
assert serializer['text'].value == ''
|
||||
assert serializer['text'].errors is None
|
||||
assert serializer['text'].name == 'text'
|
||||
assert serializer['nested']['more_text'].value == ''
|
||||
assert serializer['nested']['more_text'].errors is None
|
||||
assert serializer['nested']['more_text'].name == 'nested.more_text'
|
||||
assert serializer['nested']['amount'].value is None
|
||||
assert serializer['nested']['amount'].errors is None
|
||||
assert serializer['nested']['amount'].name == 'nested.amount'
|
1995
tests/test_fields.py
1995
tests/test_fields.py
File diff suppressed because it is too large
Load Diff
|
@ -1,92 +1,92 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.test import TestCase
|
||||
from django.utils import six
|
||||
from rest_framework import serializers
|
||||
from rest_framework.compat import BytesIO
|
||||
import datetime
|
||||
# from __future__ import unicode_literals
|
||||
# from django.test import TestCase
|
||||
# from django.utils import six
|
||||
# from rest_framework import serializers
|
||||
# from rest_framework.compat import BytesIO
|
||||
# import datetime
|
||||
|
||||
|
||||
class UploadedFile(object):
|
||||
def __init__(self, file=None, created=None):
|
||||
self.file = file
|
||||
self.created = created or datetime.datetime.now()
|
||||
# class UploadedFile(object):
|
||||
# def __init__(self, file=None, created=None):
|
||||
# self.file = file
|
||||
# self.created = created or datetime.datetime.now()
|
||||
|
||||
|
||||
class UploadedFileSerializer(serializers.Serializer):
|
||||
file = serializers.FileField(required=False)
|
||||
created = serializers.DateTimeField()
|
||||
# class UploadedFileSerializer(serializers.Serializer):
|
||||
# file = serializers.FileField(required=False)
|
||||
# created = serializers.DateTimeField()
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
if instance:
|
||||
instance.file = attrs['file']
|
||||
instance.created = attrs['created']
|
||||
return instance
|
||||
return UploadedFile(**attrs)
|
||||
# def restore_object(self, attrs, instance=None):
|
||||
# if instance:
|
||||
# instance.file = attrs['file']
|
||||
# instance.created = attrs['created']
|
||||
# return instance
|
||||
# return UploadedFile(**attrs)
|
||||
|
||||
|
||||
class FileSerializerTests(TestCase):
|
||||
def test_create(self):
|
||||
now = datetime.datetime.now()
|
||||
file = BytesIO(six.b('stuff'))
|
||||
file.name = 'stuff.txt'
|
||||
file.size = len(file.getvalue())
|
||||
serializer = UploadedFileSerializer(data={'created': now}, files={'file': file})
|
||||
uploaded_file = UploadedFile(file=file, created=now)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertEqual(serializer.object.created, uploaded_file.created)
|
||||
self.assertEqual(serializer.object.file, uploaded_file.file)
|
||||
self.assertFalse(serializer.object is uploaded_file)
|
||||
# class FileSerializerTests(TestCase):
|
||||
# def test_create(self):
|
||||
# now = datetime.datetime.now()
|
||||
# file = BytesIO(six.b('stuff'))
|
||||
# file.name = 'stuff.txt'
|
||||
# file.size = len(file.getvalue())
|
||||
# serializer = UploadedFileSerializer(data={'created': now}, files={'file': file})
|
||||
# uploaded_file = UploadedFile(file=file, created=now)
|
||||
# self.assertTrue(serializer.is_valid())
|
||||
# self.assertEqual(serializer.object.created, uploaded_file.created)
|
||||
# self.assertEqual(serializer.object.file, uploaded_file.file)
|
||||
# self.assertFalse(serializer.object is uploaded_file)
|
||||
|
||||
def test_creation_failure(self):
|
||||
"""
|
||||
Passing files=None should result in an ValidationError
|
||||
# def test_creation_failure(self):
|
||||
# """
|
||||
# Passing files=None should result in an ValidationError
|
||||
|
||||
Regression test for:
|
||||
https://github.com/tomchristie/django-rest-framework/issues/542
|
||||
"""
|
||||
now = datetime.datetime.now()
|
||||
# Regression test for:
|
||||
# https://github.com/tomchristie/django-rest-framework/issues/542
|
||||
# """
|
||||
# now = datetime.datetime.now()
|
||||
|
||||
serializer = UploadedFileSerializer(data={'created': now})
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertEqual(serializer.object.created, now)
|
||||
self.assertIsNone(serializer.object.file)
|
||||
# serializer = UploadedFileSerializer(data={'created': now})
|
||||
# self.assertTrue(serializer.is_valid())
|
||||
# self.assertEqual(serializer.object.created, now)
|
||||
# self.assertIsNone(serializer.object.file)
|
||||
|
||||
def test_remove_with_empty_string(self):
|
||||
"""
|
||||
Passing empty string as data should cause file to be removed
|
||||
# def test_remove_with_empty_string(self):
|
||||
# """
|
||||
# Passing empty string as data should cause file to be removed
|
||||
|
||||
Test for:
|
||||
https://github.com/tomchristie/django-rest-framework/issues/937
|
||||
"""
|
||||
now = datetime.datetime.now()
|
||||
file = BytesIO(six.b('stuff'))
|
||||
file.name = 'stuff.txt'
|
||||
file.size = len(file.getvalue())
|
||||
# Test for:
|
||||
# https://github.com/tomchristie/django-rest-framework/issues/937
|
||||
# """
|
||||
# now = datetime.datetime.now()
|
||||
# file = BytesIO(six.b('stuff'))
|
||||
# file.name = 'stuff.txt'
|
||||
# file.size = len(file.getvalue())
|
||||
|
||||
uploaded_file = UploadedFile(file=file, created=now)
|
||||
# uploaded_file = UploadedFile(file=file, created=now)
|
||||
|
||||
serializer = UploadedFileSerializer(instance=uploaded_file, data={'created': now, 'file': ''})
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertEqual(serializer.object.created, uploaded_file.created)
|
||||
self.assertIsNone(serializer.object.file)
|
||||
# serializer = UploadedFileSerializer(instance=uploaded_file, data={'created': now, 'file': ''})
|
||||
# self.assertTrue(serializer.is_valid())
|
||||
# self.assertEqual(serializer.object.created, uploaded_file.created)
|
||||
# self.assertIsNone(serializer.object.file)
|
||||
|
||||
def test_validation_error_with_non_file(self):
|
||||
"""
|
||||
Passing non-files should raise a validation error.
|
||||
"""
|
||||
now = datetime.datetime.now()
|
||||
errmsg = 'No file was submitted. Check the encoding type on the form.'
|
||||
# def test_validation_error_with_non_file(self):
|
||||
# """
|
||||
# Passing non-files should raise a validation error.
|
||||
# """
|
||||
# now = datetime.datetime.now()
|
||||
# errmsg = 'No file was submitted. Check the encoding type on the form.'
|
||||
|
||||
serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors, {'file': [errmsg]})
|
||||
# serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'})
|
||||
# self.assertFalse(serializer.is_valid())
|
||||
# self.assertEqual(serializer.errors, {'file': [errmsg]})
|
||||
|
||||
def test_validation_with_no_data(self):
|
||||
"""
|
||||
Validation should still function when no data dictionary is provided.
|
||||
"""
|
||||
uploaded_file = BytesIO(six.b('stuff'))
|
||||
uploaded_file.name = 'stuff.txt'
|
||||
uploaded_file.size = len(uploaded_file.getvalue())
|
||||
serializer = UploadedFileSerializer(files={'file': uploaded_file})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
# def test_validation_with_no_data(self):
|
||||
# """
|
||||
# Validation should still function when no data dictionary is provided.
|
||||
# """
|
||||
# uploaded_file = BytesIO(six.b('stuff'))
|
||||
# uploaded_file.name = 'stuff.txt'
|
||||
# uploaded_file.size = len(uploaded_file.getvalue())
|
||||
# serializer = UploadedFileSerializer(files={'file': uploaded_file})
|
||||
# self.assertFalse(serializer.is_valid())
|
||||
|
|
|
@ -2,10 +2,11 @@ from __future__ import unicode_literals
|
|||
import datetime
|
||||
from decimal import Decimal
|
||||
from django.db import models
|
||||
from django.conf.urls import patterns, url
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.utils import unittest
|
||||
from django.conf.urls import patterns, url
|
||||
from django.utils.dateparse import parse_date
|
||||
from rest_framework import generics, serializers, status, filters
|
||||
from rest_framework.compat import django_filters
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
@ -16,9 +17,14 @@ factory = APIRequestFactory()
|
|||
|
||||
|
||||
if django_filters:
|
||||
class FilterableItemSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FilterableItem
|
||||
|
||||
# Basic filter on a list view.
|
||||
class FilterFieldsRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
queryset = FilterableItem.objects.all()
|
||||
serializer_class = FilterableItemSerializer
|
||||
filter_fields = ['decimal', 'date']
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
|
||||
|
@ -33,7 +39,8 @@ if django_filters:
|
|||
fields = ['text', 'decimal', 'date']
|
||||
|
||||
class FilterClassRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
queryset = FilterableItem.objects.all()
|
||||
serializer_class = FilterableItemSerializer
|
||||
filter_class = SeveralFieldsFilter
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
|
||||
|
@ -46,12 +53,14 @@ if django_filters:
|
|||
fields = ['text']
|
||||
|
||||
class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
queryset = FilterableItem.objects.all()
|
||||
serializer_class = FilterableItemSerializer
|
||||
filter_class = MisconfiguredFilter
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
|
||||
class FilterClassDetailView(generics.RetrieveAPIView):
|
||||
model = FilterableItem
|
||||
queryset = FilterableItem.objects.all()
|
||||
serializer_class = FilterableItemSerializer
|
||||
filter_class = SeveralFieldsFilter
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
|
||||
|
@ -63,15 +72,12 @@ if django_filters:
|
|||
model = BaseFilterableItem
|
||||
|
||||
class BaseFilterableItemFilterRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
queryset = FilterableItem.objects.all()
|
||||
serializer_class = FilterableItemSerializer
|
||||
filter_class = BaseFilterableItemFilter
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
|
||||
# Regression test for #814
|
||||
class FilterableItemSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FilterableItem
|
||||
|
||||
class FilterFieldsQuerysetView(generics.ListCreateAPIView):
|
||||
queryset = FilterableItem.objects.all()
|
||||
serializer_class = FilterableItemSerializer
|
||||
|
@ -97,7 +103,7 @@ if django_filters:
|
|||
|
||||
class CommonFilteringTestCase(TestCase):
|
||||
def _serialize_object(self, obj):
|
||||
return {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
|
||||
return {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()}
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
|
@ -140,7 +146,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
request = factory.get('/', {'decimal': '%s' % search_decimal})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['decimal'] == search_decimal]
|
||||
expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
|
||||
self.assertEqual(response.data, expected_data)
|
||||
|
||||
# Tests that the date filter works.
|
||||
|
@ -148,7 +154,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-09-22'
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] == search_date]
|
||||
expected_data = [f for f in self.data if parse_date(f['date']) == search_date]
|
||||
self.assertEqual(response.data, expected_data)
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filter not installed')
|
||||
|
@ -163,7 +169,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
request = factory.get('/', {'decimal': '%s' % search_decimal})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['decimal'] == search_decimal]
|
||||
expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
|
||||
self.assertEqual(response.data, expected_data)
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filter not installed')
|
||||
|
@ -196,7 +202,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
request = factory.get('/', {'decimal': '%s' % search_decimal})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['decimal'] < search_decimal]
|
||||
expected_data = [f for f in self.data if Decimal(f['decimal']) < search_decimal]
|
||||
self.assertEqual(response.data, expected_data)
|
||||
|
||||
# Tests that the date filter set with 'gt' in the filter class works.
|
||||
|
@ -204,7 +210,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-10-02'
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] > search_date]
|
||||
expected_data = [f for f in self.data if parse_date(f['date']) > search_date]
|
||||
self.assertEqual(response.data, expected_data)
|
||||
|
||||
# Tests that the text filter set with 'icontains' in the filter class works.
|
||||
|
@ -224,8 +230,8 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] > search_date and
|
||||
f['decimal'] < search_decimal]
|
||||
expected_data = [f for f in self.data if parse_date(f['date']) > search_date and
|
||||
Decimal(f['decimal']) < search_decimal]
|
||||
self.assertEqual(response.data, expected_data)
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filter not installed')
|
||||
|
@ -323,6 +329,11 @@ class SearchFilterModel(models.Model):
|
|||
text = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class SearchFilterSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SearchFilterModel
|
||||
|
||||
|
||||
class SearchFilterTests(TestCase):
|
||||
def setUp(self):
|
||||
# Sequence of title/text is:
|
||||
|
@ -342,7 +353,8 @@ class SearchFilterTests(TestCase):
|
|||
|
||||
def test_search(self):
|
||||
class SearchListView(generics.ListAPIView):
|
||||
model = SearchFilterModel
|
||||
queryset = SearchFilterModel.objects.all()
|
||||
serializer_class = SearchFilterSerializer
|
||||
filter_backends = (filters.SearchFilter,)
|
||||
search_fields = ('title', 'text')
|
||||
|
||||
|
@ -359,7 +371,8 @@ class SearchFilterTests(TestCase):
|
|||
|
||||
def test_exact_search(self):
|
||||
class SearchListView(generics.ListAPIView):
|
||||
model = SearchFilterModel
|
||||
queryset = SearchFilterModel.objects.all()
|
||||
serializer_class = SearchFilterSerializer
|
||||
filter_backends = (filters.SearchFilter,)
|
||||
search_fields = ('=title', 'text')
|
||||
|
||||
|
@ -375,7 +388,8 @@ class SearchFilterTests(TestCase):
|
|||
|
||||
def test_startswith_search(self):
|
||||
class SearchListView(generics.ListAPIView):
|
||||
model = SearchFilterModel
|
||||
queryset = SearchFilterModel.objects.all()
|
||||
serializer_class = SearchFilterSerializer
|
||||
filter_backends = (filters.SearchFilter,)
|
||||
search_fields = ('title', '^text')
|
||||
|
||||
|
@ -392,7 +406,8 @@ class SearchFilterTests(TestCase):
|
|||
def test_search_with_nonstandard_search_param(self):
|
||||
with temporary_setting('SEARCH_PARAM', 'query', module=filters):
|
||||
class SearchListView(generics.ListAPIView):
|
||||
model = SearchFilterModel
|
||||
queryset = SearchFilterModel.objects.all()
|
||||
serializer_class = SearchFilterSerializer
|
||||
filter_backends = (filters.SearchFilter,)
|
||||
search_fields = ('title', 'text')
|
||||
|
||||
|
@ -418,6 +433,11 @@ class OrderingFilterRelatedModel(models.Model):
|
|||
related_name="relateds")
|
||||
|
||||
|
||||
class OrderingFilterSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = OrderingFilterModel
|
||||
|
||||
|
||||
class DjangoFilterOrderingModel(models.Model):
|
||||
date = models.DateField()
|
||||
text = models.CharField(max_length=10)
|
||||
|
@ -426,6 +446,11 @@ class DjangoFilterOrderingModel(models.Model):
|
|||
ordering = ['-date']
|
||||
|
||||
|
||||
class DjangoFilterOrderingSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = DjangoFilterOrderingModel
|
||||
|
||||
|
||||
class DjangoFilterOrderingTests(TestCase):
|
||||
def setUp(self):
|
||||
data = [{
|
||||
|
@ -444,7 +469,8 @@ class DjangoFilterOrderingTests(TestCase):
|
|||
|
||||
def test_default_ordering(self):
|
||||
class DjangoFilterOrderingView(generics.ListAPIView):
|
||||
model = DjangoFilterOrderingModel
|
||||
serializer_class = DjangoFilterOrderingSerializer
|
||||
queryset = DjangoFilterOrderingModel.objects.all()
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filter_fields = ['text']
|
||||
ordering = ('-date',)
|
||||
|
@ -456,9 +482,9 @@ class DjangoFilterOrderingTests(TestCase):
|
|||
self.assertEqual(
|
||||
response.data,
|
||||
[
|
||||
{'id': 3, 'date': datetime.date(2014, 10, 8), 'text': 'cde'},
|
||||
{'id': 2, 'date': datetime.date(2013, 10, 8), 'text': 'bcd'},
|
||||
{'id': 1, 'date': datetime.date(2012, 10, 8), 'text': 'abc'}
|
||||
{'id': 3, 'date': '2014-10-08', 'text': 'cde'},
|
||||
{'id': 2, 'date': '2013-10-08', 'text': 'bcd'},
|
||||
{'id': 1, 'date': '2012-10-08', 'text': 'abc'}
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -485,7 +511,8 @@ class OrderingFilterTests(TestCase):
|
|||
|
||||
def test_ordering(self):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
model = OrderingFilterModel
|
||||
queryset = OrderingFilterModel.objects.all()
|
||||
serializer_class = OrderingFilterSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
ordering_fields = ('text',)
|
||||
|
@ -504,7 +531,8 @@ class OrderingFilterTests(TestCase):
|
|||
|
||||
def test_reverse_ordering(self):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
model = OrderingFilterModel
|
||||
queryset = OrderingFilterModel.objects.all()
|
||||
serializer_class = OrderingFilterSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
ordering_fields = ('text',)
|
||||
|
@ -523,7 +551,8 @@ class OrderingFilterTests(TestCase):
|
|||
|
||||
def test_incorrectfield_ordering(self):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
model = OrderingFilterModel
|
||||
queryset = OrderingFilterModel.objects.all()
|
||||
serializer_class = OrderingFilterSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
ordering_fields = ('text',)
|
||||
|
@ -542,7 +571,8 @@ class OrderingFilterTests(TestCase):
|
|||
|
||||
def test_default_ordering(self):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
model = OrderingFilterModel
|
||||
queryset = OrderingFilterModel.objects.all()
|
||||
serializer_class = OrderingFilterSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
oredering_fields = ('text',)
|
||||
|
@ -561,7 +591,8 @@ class OrderingFilterTests(TestCase):
|
|||
|
||||
def test_default_ordering_using_string(self):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
model = OrderingFilterModel
|
||||
queryset = OrderingFilterModel.objects.all()
|
||||
serializer_class = OrderingFilterSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = 'title'
|
||||
ordering_fields = ('text',)
|
||||
|
@ -590,7 +621,7 @@ class OrderingFilterTests(TestCase):
|
|||
new_related.save()
|
||||
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
model = OrderingFilterModel
|
||||
serializer_class = OrderingFilterSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = 'title'
|
||||
ordering_fields = '__all__'
|
||||
|
@ -612,7 +643,8 @@ class OrderingFilterTests(TestCase):
|
|||
def test_ordering_with_nonstandard_ordering_param(self):
|
||||
with temporary_setting('ORDERING_PARAM', 'order', filters):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
model = OrderingFilterModel
|
||||
queryset = OrderingFilterModel.objects.all()
|
||||
serializer_class = OrderingFilterSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
ordering_fields = ('text',)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
import django
|
||||
from django.db import models
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.test import TestCase
|
||||
|
@ -11,44 +12,44 @@ from tests.models import ForeignKeySource, ForeignKeyTarget
|
|||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class BasicSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BasicModel
|
||||
|
||||
|
||||
class ForeignKeySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ForeignKeySource
|
||||
|
||||
|
||||
class RootView(generics.ListCreateAPIView):
|
||||
"""
|
||||
Example description for OPTIONS.
|
||||
"""
|
||||
model = BasicModel
|
||||
queryset = BasicModel.objects.all()
|
||||
serializer_class = BasicSerializer
|
||||
|
||||
|
||||
class InstanceView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
Example description for OPTIONS.
|
||||
"""
|
||||
model = BasicModel
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super(InstanceView, self).get_queryset()
|
||||
return queryset.exclude(text='filtered out')
|
||||
queryset = BasicModel.objects.exclude(text='filtered out')
|
||||
serializer_class = BasicSerializer
|
||||
|
||||
|
||||
class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
FK: example description for OPTIONS.
|
||||
"""
|
||||
model = ForeignKeySource
|
||||
queryset = ForeignKeySource.objects.all()
|
||||
serializer_class = ForeignKeySerializer
|
||||
|
||||
|
||||
class SlugSerializer(serializers.ModelSerializer):
|
||||
slug = serializers.Field() # read only
|
||||
slug = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = SlugBasedModel
|
||||
exclude = ('id',)
|
||||
fields = ('text', 'slug')
|
||||
|
||||
|
||||
class SlugBasedInstanceView(InstanceView):
|
||||
"""
|
||||
A model with a slug-field.
|
||||
"""
|
||||
model = SlugBasedModel
|
||||
queryset = SlugBasedModel.objects.all()
|
||||
serializer_class = SlugSerializer
|
||||
lookup_field = 'slug'
|
||||
|
||||
|
@ -112,47 +113,6 @@ class TestRootView(TestCase):
|
|||
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||
self.assertEqual(response.data, {"detail": "Method 'DELETE' not allowed."})
|
||||
|
||||
def test_options_root_view(self):
|
||||
"""
|
||||
OPTIONS requests to ListCreateAPIView should return metadata
|
||||
"""
|
||||
request = factory.options('/')
|
||||
with self.assertNumQueries(0):
|
||||
response = self.view(request).render()
|
||||
expected = {
|
||||
'parses': [
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data'
|
||||
],
|
||||
'renders': [
|
||||
'application/json',
|
||||
'text/html'
|
||||
],
|
||||
'name': 'Root',
|
||||
'description': 'Example description for OPTIONS.',
|
||||
'actions': {
|
||||
'POST': {
|
||||
'text': {
|
||||
'max_length': 100,
|
||||
'read_only': False,
|
||||
'required': True,
|
||||
'type': 'string',
|
||||
"label": "Text comes here",
|
||||
"help_text": "Text description."
|
||||
},
|
||||
'id': {
|
||||
'read_only': True,
|
||||
'required': False,
|
||||
'type': 'integer',
|
||||
'label': 'ID',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
def test_post_cannot_set_id(self):
|
||||
"""
|
||||
POST requests to create a new object should not be able to set the id.
|
||||
|
@ -167,6 +127,9 @@ class TestRootView(TestCase):
|
|||
self.assertEqual(created.text, 'foobar')
|
||||
|
||||
|
||||
EXPECTED_QUERYS_FOR_PUT = 3 if django.VERSION < (1, 6) else 2
|
||||
|
||||
|
||||
class TestInstanceView(TestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
|
@ -210,10 +173,10 @@ class TestInstanceView(TestCase):
|
|||
"""
|
||||
data = {'text': 'foobar'}
|
||||
request = factory.put('/1', data, format='json')
|
||||
with self.assertNumQueries(2):
|
||||
with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT):
|
||||
response = self.view(request, pk='1').render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||
self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'})
|
||||
updated = self.objects.get(id=1)
|
||||
self.assertEqual(updated.text, 'foobar')
|
||||
|
||||
|
@ -224,7 +187,7 @@ class TestInstanceView(TestCase):
|
|||
data = {'text': 'foobar'}
|
||||
request = factory.patch('/1', data, format='json')
|
||||
|
||||
with self.assertNumQueries(2):
|
||||
with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT):
|
||||
response = self.view(request, pk=1).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||
|
@ -243,89 +206,6 @@ class TestInstanceView(TestCase):
|
|||
ids = [obj.id for obj in self.objects.all()]
|
||||
self.assertEqual(ids, [2, 3])
|
||||
|
||||
def test_options_instance_view(self):
|
||||
"""
|
||||
OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
|
||||
"""
|
||||
request = factory.options('/1')
|
||||
with self.assertNumQueries(1):
|
||||
response = self.view(request, pk=1).render()
|
||||
expected = {
|
||||
'parses': [
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data'
|
||||
],
|
||||
'renders': [
|
||||
'application/json',
|
||||
'text/html'
|
||||
],
|
||||
'name': 'Instance',
|
||||
'description': 'Example description for OPTIONS.',
|
||||
'actions': {
|
||||
'PUT': {
|
||||
'text': {
|
||||
'max_length': 100,
|
||||
'read_only': False,
|
||||
'required': True,
|
||||
'type': 'string',
|
||||
'label': 'Text comes here',
|
||||
'help_text': 'Text description.'
|
||||
},
|
||||
'id': {
|
||||
'read_only': True,
|
||||
'required': False,
|
||||
'type': 'integer',
|
||||
'label': 'ID',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
def test_options_before_instance_create(self):
|
||||
"""
|
||||
OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
|
||||
before the instance has been created
|
||||
"""
|
||||
request = factory.options('/999')
|
||||
with self.assertNumQueries(1):
|
||||
response = self.view(request, pk=999).render()
|
||||
expected = {
|
||||
'parses': [
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data'
|
||||
],
|
||||
'renders': [
|
||||
'application/json',
|
||||
'text/html'
|
||||
],
|
||||
'name': 'Instance',
|
||||
'description': 'Example description for OPTIONS.',
|
||||
'actions': {
|
||||
'PUT': {
|
||||
'text': {
|
||||
'max_length': 100,
|
||||
'read_only': False,
|
||||
'required': True,
|
||||
'type': 'string',
|
||||
'label': 'Text comes here',
|
||||
'help_text': 'Text description.'
|
||||
},
|
||||
'id': {
|
||||
'read_only': True,
|
||||
'required': False,
|
||||
'type': 'integer',
|
||||
'label': 'ID',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
def test_get_instance_view_incorrect_arg(self):
|
||||
"""
|
||||
GET requests with an incorrect pk type, should raise 404, not 500.
|
||||
|
@ -342,7 +222,7 @@ class TestInstanceView(TestCase):
|
|||
"""
|
||||
data = {'id': 999, 'text': 'foobar'}
|
||||
request = factory.put('/1', data, format='json')
|
||||
with self.assertNumQueries(2):
|
||||
with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT):
|
||||
response = self.view(request, pk=1).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||
|
@ -351,18 +231,15 @@ class TestInstanceView(TestCase):
|
|||
|
||||
def test_put_to_deleted_instance(self):
|
||||
"""
|
||||
PUT requests to RetrieveUpdateDestroyAPIView should create an object
|
||||
if it does not currently exist.
|
||||
PUT requests to RetrieveUpdateDestroyAPIView should return 404 if
|
||||
an object does not currently exist.
|
||||
"""
|
||||
self.objects.get(id=1).delete()
|
||||
data = {'text': 'foobar'}
|
||||
request = factory.put('/1', data, format='json')
|
||||
with self.assertNumQueries(3):
|
||||
with self.assertNumQueries(1):
|
||||
response = self.view(request, pk=1).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||
updated = self.objects.get(id=1)
|
||||
self.assertEqual(updated.text, 'foobar')
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_put_to_filtered_out_instance(self):
|
||||
"""
|
||||
|
@ -373,35 +250,7 @@ class TestInstanceView(TestCase):
|
|||
filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk
|
||||
request = factory.put('/{0}'.format(filtered_out_pk), data, format='json')
|
||||
response = self.view(request, pk=filtered_out_pk).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_put_as_create_on_id_based_url(self):
|
||||
"""
|
||||
PUT requests to RetrieveUpdateDestroyAPIView should create an object
|
||||
at the requested url if it doesn't exist.
|
||||
"""
|
||||
data = {'text': 'foobar'}
|
||||
# pk fields can not be created on demand, only the database can set the pk for a new object
|
||||
request = factory.put('/5', data, format='json')
|
||||
with self.assertNumQueries(3):
|
||||
response = self.view(request, pk=5).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
new_obj = self.objects.get(pk=5)
|
||||
self.assertEqual(new_obj.text, 'foobar')
|
||||
|
||||
def test_put_as_create_on_slug_based_url(self):
|
||||
"""
|
||||
PUT requests to RetrieveUpdateDestroyAPIView should create an object
|
||||
at the requested url if possible, else return HTTP_403_FORBIDDEN error-response.
|
||||
"""
|
||||
data = {'text': 'foobar'}
|
||||
request = factory.put('/test_slug', data, format='json')
|
||||
with self.assertNumQueries(2):
|
||||
response = self.slug_based_view(request, slug='test_slug').render()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data, {'slug': 'test_slug', 'text': 'foobar'})
|
||||
new_obj = SlugBasedModel.objects.get(slug='test_slug')
|
||||
self.assertEqual(new_obj.text, 'foobar')
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_patch_cannot_create_an_object(self):
|
||||
"""
|
||||
|
@ -433,53 +282,6 @@ class TestFKInstanceView(TestCase):
|
|||
]
|
||||
self.view = FKInstanceView.as_view()
|
||||
|
||||
def test_options_root_view(self):
|
||||
"""
|
||||
OPTIONS requests to ListCreateAPIView should return metadata
|
||||
"""
|
||||
request = factory.options('/999')
|
||||
with self.assertNumQueries(1):
|
||||
response = self.view(request, pk=999).render()
|
||||
expected = {
|
||||
'name': 'Fk Instance',
|
||||
'description': 'FK: example description for OPTIONS.',
|
||||
'renders': [
|
||||
'application/json',
|
||||
'text/html'
|
||||
],
|
||||
'parses': [
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data'
|
||||
],
|
||||
'actions': {
|
||||
'PUT': {
|
||||
'id': {
|
||||
'type': 'integer',
|
||||
'required': False,
|
||||
'read_only': True,
|
||||
'label': 'ID'
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'read_only': False,
|
||||
'label': 'name',
|
||||
'max_length': 100
|
||||
},
|
||||
'target': {
|
||||
'type': 'field',
|
||||
'required': True,
|
||||
'read_only': False,
|
||||
'label': 'Target',
|
||||
'help_text': 'Target'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
|
||||
class TestOverriddenGetObject(TestCase):
|
||||
"""
|
||||
|
@ -503,7 +305,7 @@ class TestOverriddenGetObject(TestCase):
|
|||
"""
|
||||
Example detail view for override of get_object().
|
||||
"""
|
||||
model = BasicModel
|
||||
serializer_class = BasicSerializer
|
||||
|
||||
def get_object(self):
|
||||
pk = int(self.kwargs['pk'])
|
||||
|
@ -565,7 +367,9 @@ class ClassA(models.Model):
|
|||
|
||||
|
||||
class ClassASerializer(serializers.ModelSerializer):
|
||||
childs = serializers.PrimaryKeyRelatedField(many=True, source='childs')
|
||||
childs = serializers.PrimaryKeyRelatedField(
|
||||
many=True, queryset=ClassB.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ClassA
|
||||
|
@ -573,7 +377,7 @@ class ClassASerializer(serializers.ModelSerializer):
|
|||
|
||||
class ExampleView(generics.ListCreateAPIView):
|
||||
serializer_class = ClassASerializer
|
||||
model = ClassA
|
||||
queryset = ClassA.objects.all()
|
||||
|
||||
|
||||
class TestM2MBrowseableAPI(TestCase):
|
||||
|
@ -603,7 +407,7 @@ class TwoFieldModel(models.Model):
|
|||
|
||||
|
||||
class DynamicSerializerView(generics.ListCreateAPIView):
|
||||
model = TwoFieldModel
|
||||
queryset = TwoFieldModel.objects.all()
|
||||
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
|
||||
|
||||
def get_serializer_class(self):
|
||||
|
@ -612,8 +416,11 @@ class DynamicSerializerView(generics.ListCreateAPIView):
|
|||
class Meta:
|
||||
model = TwoFieldModel
|
||||
fields = ('field_b',)
|
||||
return DynamicSerializer
|
||||
return super(DynamicSerializerView, self).get_serializer_class()
|
||||
else:
|
||||
class DynamicSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = TwoFieldModel
|
||||
return DynamicSerializer
|
||||
|
||||
|
||||
class TestFilterBackendAppliedToViews(TestCase):
|
||||
|
@ -681,42 +488,3 @@ class TestFilterBackendAppliedToViews(TestCase):
|
|||
response = view(request).render()
|
||||
self.assertContains(response, 'field_b')
|
||||
self.assertNotContains(response, 'field_a')
|
||||
|
||||
def test_options_with_dynamic_serializer(self):
|
||||
"""
|
||||
Ensure that OPTIONS returns correct POST json schema:
|
||||
DynamicSerializer with single field 'field_b'
|
||||
"""
|
||||
request = factory.options('/')
|
||||
view = DynamicSerializerView.as_view()
|
||||
|
||||
with self.assertNumQueries(0):
|
||||
response = view(request).render()
|
||||
|
||||
expected = {
|
||||
'name': 'Dynamic Serializer',
|
||||
'description': '',
|
||||
'renders': [
|
||||
'text/html',
|
||||
'application/json'
|
||||
],
|
||||
'parses': [
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data'
|
||||
],
|
||||
'actions': {
|
||||
'POST': {
|
||||
'field_b': {
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'read_only': False,
|
||||
'label': 'field b',
|
||||
'max_length': 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
|
|
@ -1,380 +1,406 @@
|
|||
from __future__ import unicode_literals
|
||||
import json
|
||||
from django.test import TestCase
|
||||
from rest_framework import generics, status, serializers
|
||||
from django.conf.urls import patterns, url
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from tests.models import (
|
||||
Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment,
|
||||
Album, Photo, OptionalRelationModel
|
||||
)
|
||||
# from __future__ import unicode_literals
|
||||
# import json
|
||||
# from django.test import TestCase
|
||||
# from rest_framework import generics, status, serializers
|
||||
# from django.conf.urls import patterns, url
|
||||
# from rest_framework.settings import api_settings
|
||||
# from rest_framework.test import APIRequestFactory
|
||||
# from tests.models import (
|
||||
# Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment,
|
||||
# Album, Photo, OptionalRelationModel
|
||||
# )
|
||||
|
||||
factory = APIRequestFactory()
|
||||
# factory = APIRequestFactory()
|
||||
|
||||
|
||||
class BlogPostCommentSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='blogpostcomment-detail')
|
||||
text = serializers.CharField()
|
||||
blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail')
|
||||
# class BlogPostCommentSerializer(serializers.ModelSerializer):
|
||||
# url = serializers.HyperlinkedIdentityField(view_name='blogpostcomment-detail')
|
||||
# text = serializers.CharField()
|
||||
# blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail')
|
||||
|
||||
class Meta:
|
||||
model = BlogPostComment
|
||||
fields = ('text', 'blog_post_url', 'url')
|
||||
# class Meta:
|
||||
# model = BlogPostComment
|
||||
# fields = ('text', 'blog_post_url', 'url')
|
||||
|
||||
|
||||
class PhotoSerializer(serializers.Serializer):
|
||||
description = serializers.CharField()
|
||||
album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), lookup_field='title')
|
||||
# class PhotoSerializer(serializers.Serializer):
|
||||
# description = serializers.CharField()
|
||||
# album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), lookup_field='title')
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
return Photo(**attrs)
|
||||
# def restore_object(self, attrs, instance=None):
|
||||
# return Photo(**attrs)
|
||||
|
||||
|
||||
class AlbumSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='album-detail', lookup_field='title')
|
||||
# class AlbumSerializer(serializers.ModelSerializer):
|
||||
# url = serializers.HyperlinkedIdentityField(view_name='album-detail', lookup_field='title')
|
||||
|
||||
class Meta:
|
||||
model = Album
|
||||
fields = ('title', 'url')
|
||||
# class Meta:
|
||||
# model = Album
|
||||
# fields = ('title', 'url')
|
||||
|
||||
|
||||
class BasicList(generics.ListCreateAPIView):
|
||||
model = BasicModel
|
||||
model_serializer_class = serializers.HyperlinkedModelSerializer
|
||||
# class BasicSerializer(serializers.HyperlinkedModelSerializer):
|
||||
# class Meta:
|
||||
# model = BasicModel
|
||||
|
||||
|
||||
class BasicDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
model = BasicModel
|
||||
model_serializer_class = serializers.HyperlinkedModelSerializer
|
||||
# class AnchorSerializer(serializers.HyperlinkedModelSerializer):
|
||||
# class Meta:
|
||||
# model = Anchor
|
||||
|
||||
|
||||
class AnchorDetail(generics.RetrieveAPIView):
|
||||
model = Anchor
|
||||
model_serializer_class = serializers.HyperlinkedModelSerializer
|
||||
# class ManyToManySerializer(serializers.HyperlinkedModelSerializer):
|
||||
# class Meta:
|
||||
# model = ManyToManyModel
|
||||
|
||||
|
||||
class ManyToManyList(generics.ListAPIView):
|
||||
model = ManyToManyModel
|
||||
model_serializer_class = serializers.HyperlinkedModelSerializer
|
||||
# class BlogPostSerializer(serializers.ModelSerializer):
|
||||
# class Meta:
|
||||
# model = BlogPost
|
||||
|
||||
|
||||
class ManyToManyDetail(generics.RetrieveAPIView):
|
||||
model = ManyToManyModel
|
||||
model_serializer_class = serializers.HyperlinkedModelSerializer
|
||||
# class OptionalRelationSerializer(serializers.HyperlinkedModelSerializer):
|
||||
# class Meta:
|
||||
# model = OptionalRelationModel
|
||||
|
||||
|
||||
class BlogPostCommentListCreate(generics.ListCreateAPIView):
|
||||
model = BlogPostComment
|
||||
serializer_class = BlogPostCommentSerializer
|
||||
# class BasicList(generics.ListCreateAPIView):
|
||||
# queryset = BasicModel.objects.all()
|
||||
# serializer_class = BasicSerializer
|
||||
|
||||
|
||||
class BlogPostCommentDetail(generics.RetrieveAPIView):
|
||||
model = BlogPostComment
|
||||
serializer_class = BlogPostCommentSerializer
|
||||
# class BasicDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
# queryset = BasicModel.objects.all()
|
||||
# serializer_class = BasicSerializer
|
||||
|
||||
|
||||
class BlogPostDetail(generics.RetrieveAPIView):
|
||||
model = BlogPost
|
||||
# class AnchorDetail(generics.RetrieveAPIView):
|
||||
# queryset = Anchor.objects.all()
|
||||
# serializer_class = AnchorSerializer
|
||||
|
||||
|
||||
class PhotoListCreate(generics.ListCreateAPIView):
|
||||
model = Photo
|
||||
model_serializer_class = PhotoSerializer
|
||||
|
||||
|
||||
class AlbumDetail(generics.RetrieveAPIView):
|
||||
model = Album
|
||||
serializer_class = AlbumSerializer
|
||||
lookup_field = 'title'
|
||||
|
||||
|
||||
class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
model = OptionalRelationModel
|
||||
model_serializer_class = serializers.HyperlinkedModelSerializer
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),
|
||||
url(r'^basic/(?P<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'),
|
||||
url(r'^anchor/(?P<pk>\d+)/$', AnchorDetail.as_view(), name='anchor-detail'),
|
||||
url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'),
|
||||
url(r'^manytomany/(?P<pk>\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'),
|
||||
url(r'^posts/(?P<pk>\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'),
|
||||
url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'),
|
||||
url(r'^comments/(?P<pk>\d+)/$', BlogPostCommentDetail.as_view(), name='blogpostcomment-detail'),
|
||||
url(r'^albums/(?P<title>\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'),
|
||||
url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list'),
|
||||
url(r'^optionalrelation/(?P<pk>\d+)/$', OptionalRelationDetail.as_view(), name='optionalrelationmodel-detail'),
|
||||
)
|
||||
|
||||
|
||||
class TestBasicHyperlinkedView(TestCase):
|
||||
urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 3 BasicModel instances.
|
||||
"""
|
||||
items = ['foo', 'bar', 'baz']
|
||||
for item in items:
|
||||
BasicModel(text=item).save()
|
||||
self.objects = BasicModel.objects
|
||||
self.data = [
|
||||
{'url': 'http://testserver/basic/%d/' % obj.id, 'text': obj.text}
|
||||
for obj in self.objects.all()
|
||||
]
|
||||
self.list_view = BasicList.as_view()
|
||||
self.detail_view = BasicDetail.as_view()
|
||||
|
||||
def test_get_list_view(self):
|
||||
"""
|
||||
GET requests to ListCreateAPIView should return list of objects.
|
||||
"""
|
||||
request = factory.get('/basic/')
|
||||
response = self.list_view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, self.data)
|
||||
|
||||
def test_get_detail_view(self):
|
||||
"""
|
||||
GET requests to ListCreateAPIView should return list of objects.
|
||||
"""
|
||||
request = factory.get('/basic/1')
|
||||
response = self.detail_view(request, pk=1).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, self.data[0])
|
||||
|
||||
|
||||
class TestManyToManyHyperlinkedView(TestCase):
|
||||
urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 3 BasicModel instances.
|
||||
"""
|
||||
items = ['foo', 'bar', 'baz']
|
||||
anchors = []
|
||||
for item in items:
|
||||
anchor = Anchor(text=item)
|
||||
anchor.save()
|
||||
anchors.append(anchor)
|
||||
|
||||
manytomany = ManyToManyModel()
|
||||
manytomany.save()
|
||||
manytomany.rel.add(*anchors)
|
||||
|
||||
self.data = [{
|
||||
'url': 'http://testserver/manytomany/1/',
|
||||
'rel': [
|
||||
'http://testserver/anchor/1/',
|
||||
'http://testserver/anchor/2/',
|
||||
'http://testserver/anchor/3/',
|
||||
]
|
||||
}]
|
||||
self.list_view = ManyToManyList.as_view()
|
||||
self.detail_view = ManyToManyDetail.as_view()
|
||||
|
||||
def test_get_list_view(self):
|
||||
"""
|
||||
GET requests to ListCreateAPIView should return list of objects.
|
||||
"""
|
||||
request = factory.get('/manytomany/')
|
||||
response = self.list_view(request)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, self.data)
|
||||
|
||||
def test_get_detail_view(self):
|
||||
"""
|
||||
GET requests to ListCreateAPIView should return list of objects.
|
||||
"""
|
||||
request = factory.get('/manytomany/1/')
|
||||
response = self.detail_view(request, pk=1)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, self.data[0])
|
||||
|
||||
|
||||
class TestHyperlinkedIdentityFieldLookup(TestCase):
|
||||
urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 3 Album instances.
|
||||
"""
|
||||
titles = ['foo', 'bar', 'baz']
|
||||
for title in titles:
|
||||
album = Album(title=title)
|
||||
album.save()
|
||||
self.detail_view = AlbumDetail.as_view()
|
||||
self.data = {
|
||||
'foo': {'title': 'foo', 'url': 'http://testserver/albums/foo/'},
|
||||
'bar': {'title': 'bar', 'url': 'http://testserver/albums/bar/'},
|
||||
'baz': {'title': 'baz', 'url': 'http://testserver/albums/baz/'}
|
||||
}
|
||||
|
||||
def test_lookup_field(self):
|
||||
"""
|
||||
GET requests to AlbumDetail view should return serialized Albums
|
||||
with a url field keyed by `title`.
|
||||
"""
|
||||
for album in Album.objects.all():
|
||||
request = factory.get('/albums/{0}/'.format(album.title))
|
||||
response = self.detail_view(request, title=album.title)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, self.data[album.title])
|
||||
|
||||
|
||||
class TestCreateWithForeignKeys(TestCase):
|
||||
urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a blog post
|
||||
"""
|
||||
self.post = BlogPost.objects.create(title="Test post")
|
||||
self.create_view = BlogPostCommentListCreate.as_view()
|
||||
|
||||
def test_create_comment(self):
|
||||
|
||||
data = {
|
||||
'text': 'A test comment',
|
||||
'blog_post_url': 'http://testserver/posts/1/'
|
||||
}
|
||||
|
||||
request = factory.post('/comments/', data=data)
|
||||
response = self.create_view(request)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response['Location'], 'http://testserver/comments/1/')
|
||||
self.assertEqual(self.post.blogpostcomment_set.count(), 1)
|
||||
self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment')
|
||||
|
||||
|
||||
class TestCreateWithForeignKeysAndCustomSlug(TestCase):
|
||||
urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create an Album
|
||||
"""
|
||||
self.post = Album.objects.create(title='test-album')
|
||||
self.list_create_view = PhotoListCreate.as_view()
|
||||
|
||||
def test_create_photo(self):
|
||||
|
||||
data = {
|
||||
'description': 'A test photo',
|
||||
'album_url': 'http://testserver/albums/test-album/'
|
||||
}
|
||||
|
||||
request = factory.post('/photos/', data=data)
|
||||
response = self.list_create_view(request)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertNotIn('Location', response, msg='Location should only be included if there is a "url" field on the serializer')
|
||||
self.assertEqual(self.post.photo_set.count(), 1)
|
||||
self.assertEqual(self.post.photo_set.all()[0].description, 'A test photo')
|
||||
|
||||
|
||||
class TestOptionalRelationHyperlinkedView(TestCase):
|
||||
urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 1 OptionalRelationModel instances.
|
||||
"""
|
||||
OptionalRelationModel().save()
|
||||
self.objects = OptionalRelationModel.objects
|
||||
self.detail_view = OptionalRelationDetail.as_view()
|
||||
self.data = {"url": "http://testserver/optionalrelation/1/", "other": None}
|
||||
|
||||
def test_get_detail_view(self):
|
||||
"""
|
||||
GET requests to RetrieveAPIView with optional relations should return None
|
||||
for non existing relations.
|
||||
"""
|
||||
request = factory.get('/optionalrelationmodel-detail/1')
|
||||
response = self.detail_view(request, pk=1)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, self.data)
|
||||
|
||||
def test_put_detail_view(self):
|
||||
"""
|
||||
PUT requests to RetrieveUpdateDestroyAPIView with optional relations
|
||||
should accept None for non existing relations.
|
||||
"""
|
||||
response = self.client.put('/optionalrelation/1/',
|
||||
data=json.dumps(self.data),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class TestOverriddenURLField(TestCase):
|
||||
def setUp(self):
|
||||
class OverriddenURLSerializer(serializers.HyperlinkedModelSerializer):
|
||||
url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
class Meta:
|
||||
model = BlogPost
|
||||
fields = ('title', 'url')
|
||||
|
||||
def get_url(self, obj):
|
||||
return 'foo bar'
|
||||
|
||||
self.Serializer = OverriddenURLSerializer
|
||||
self.obj = BlogPost.objects.create(title='New blog post')
|
||||
|
||||
def test_overridden_url_field(self):
|
||||
"""
|
||||
The 'url' field should respect overriding.
|
||||
Regression test for #936.
|
||||
"""
|
||||
serializer = self.Serializer(self.obj)
|
||||
self.assertEqual(
|
||||
serializer.data,
|
||||
{'title': 'New blog post', 'url': 'foo bar'}
|
||||
)
|
||||
|
||||
|
||||
class TestURLFieldNameBySettings(TestCase):
|
||||
urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
def setUp(self):
|
||||
self.saved_url_field_name = api_settings.URL_FIELD_NAME
|
||||
api_settings.URL_FIELD_NAME = 'global_url_field'
|
||||
|
||||
class Serializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = BlogPost
|
||||
fields = ('title', api_settings.URL_FIELD_NAME)
|
||||
|
||||
self.Serializer = Serializer
|
||||
self.obj = BlogPost.objects.create(title="New blog post")
|
||||
|
||||
def tearDown(self):
|
||||
api_settings.URL_FIELD_NAME = self.saved_url_field_name
|
||||
|
||||
def test_overridden_url_field_name(self):
|
||||
request = factory.get('/posts/')
|
||||
serializer = self.Serializer(self.obj, context={'request': request})
|
||||
self.assertIn(api_settings.URL_FIELD_NAME, serializer.data)
|
||||
|
||||
|
||||
class TestURLFieldNameByOptions(TestCase):
|
||||
urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
def setUp(self):
|
||||
class Serializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = BlogPost
|
||||
fields = ('title', 'serializer_url_field')
|
||||
url_field_name = 'serializer_url_field'
|
||||
|
||||
self.Serializer = Serializer
|
||||
self.obj = BlogPost.objects.create(title="New blog post")
|
||||
|
||||
def test_overridden_url_field_name(self):
|
||||
request = factory.get('/posts/')
|
||||
serializer = self.Serializer(self.obj, context={'request': request})
|
||||
self.assertIn(self.Serializer.Meta.url_field_name, serializer.data)
|
||||
# class ManyToManyList(generics.ListAPIView):
|
||||
# queryset = ManyToManyModel.objects.all()
|
||||
# serializer_class = ManyToManySerializer
|
||||
|
||||
|
||||
# class ManyToManyDetail(generics.RetrieveAPIView):
|
||||
# queryset = ManyToManyModel.objects.all()
|
||||
# serializer_class = ManyToManySerializer
|
||||
|
||||
|
||||
# class BlogPostCommentListCreate(generics.ListCreateAPIView):
|
||||
# queryset = BlogPostComment.objects.all()
|
||||
# serializer_class = BlogPostCommentSerializer
|
||||
|
||||
|
||||
# class BlogPostCommentDetail(generics.RetrieveAPIView):
|
||||
# queryset = BlogPostComment.objects.all()
|
||||
# serializer_class = BlogPostCommentSerializer
|
||||
|
||||
|
||||
# class BlogPostDetail(generics.RetrieveAPIView):
|
||||
# queryset = BlogPost.objects.all()
|
||||
# serializer_class = BlogPostSerializer
|
||||
|
||||
|
||||
# class PhotoListCreate(generics.ListCreateAPIView):
|
||||
# queryset = Photo.objects.all()
|
||||
# serializer_class = PhotoSerializer
|
||||
|
||||
|
||||
# class AlbumDetail(generics.RetrieveAPIView):
|
||||
# queryset = Album.objects.all()
|
||||
# serializer_class = AlbumSerializer
|
||||
# lookup_field = 'title'
|
||||
|
||||
|
||||
# class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
# queryset = OptionalRelationModel.objects.all()
|
||||
# serializer_class = OptionalRelationSerializer
|
||||
|
||||
|
||||
# urlpatterns = patterns(
|
||||
# '',
|
||||
# url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),
|
||||
# url(r'^basic/(?P<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'),
|
||||
# url(r'^anchor/(?P<pk>\d+)/$', AnchorDetail.as_view(), name='anchor-detail'),
|
||||
# url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'),
|
||||
# url(r'^manytomany/(?P<pk>\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'),
|
||||
# url(r'^posts/(?P<pk>\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'),
|
||||
# url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'),
|
||||
# url(r'^comments/(?P<pk>\d+)/$', BlogPostCommentDetail.as_view(), name='blogpostcomment-detail'),
|
||||
# url(r'^albums/(?P<title>\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'),
|
||||
# url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list'),
|
||||
# url(r'^optionalrelation/(?P<pk>\d+)/$', OptionalRelationDetail.as_view(), name='optionalrelationmodel-detail'),
|
||||
# )
|
||||
|
||||
|
||||
# class TestBasicHyperlinkedView(TestCase):
|
||||
# urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
# def setUp(self):
|
||||
# """
|
||||
# Create 3 BasicModel instances.
|
||||
# """
|
||||
# items = ['foo', 'bar', 'baz']
|
||||
# for item in items:
|
||||
# BasicModel(text=item).save()
|
||||
# self.objects = BasicModel.objects
|
||||
# self.data = [
|
||||
# {'url': 'http://testserver/basic/%d/' % obj.id, 'text': obj.text}
|
||||
# for obj in self.objects.all()
|
||||
# ]
|
||||
# self.list_view = BasicList.as_view()
|
||||
# self.detail_view = BasicDetail.as_view()
|
||||
|
||||
# def test_get_list_view(self):
|
||||
# """
|
||||
# GET requests to ListCreateAPIView should return list of objects.
|
||||
# """
|
||||
# request = factory.get('/basic/')
|
||||
# response = self.list_view(request).render()
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# self.assertEqual(response.data, self.data)
|
||||
|
||||
# def test_get_detail_view(self):
|
||||
# """
|
||||
# GET requests to ListCreateAPIView should return list of objects.
|
||||
# """
|
||||
# request = factory.get('/basic/1')
|
||||
# response = self.detail_view(request, pk=1).render()
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# self.assertEqual(response.data, self.data[0])
|
||||
|
||||
|
||||
# class TestManyToManyHyperlinkedView(TestCase):
|
||||
# urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
# def setUp(self):
|
||||
# """
|
||||
# Create 3 BasicModel instances.
|
||||
# """
|
||||
# items = ['foo', 'bar', 'baz']
|
||||
# anchors = []
|
||||
# for item in items:
|
||||
# anchor = Anchor(text=item)
|
||||
# anchor.save()
|
||||
# anchors.append(anchor)
|
||||
|
||||
# manytomany = ManyToManyModel()
|
||||
# manytomany.save()
|
||||
# manytomany.rel.add(*anchors)
|
||||
|
||||
# self.data = [{
|
||||
# 'url': 'http://testserver/manytomany/1/',
|
||||
# 'rel': [
|
||||
# 'http://testserver/anchor/1/',
|
||||
# 'http://testserver/anchor/2/',
|
||||
# 'http://testserver/anchor/3/',
|
||||
# ]
|
||||
# }]
|
||||
# self.list_view = ManyToManyList.as_view()
|
||||
# self.detail_view = ManyToManyDetail.as_view()
|
||||
|
||||
# def test_get_list_view(self):
|
||||
# """
|
||||
# GET requests to ListCreateAPIView should return list of objects.
|
||||
# """
|
||||
# request = factory.get('/manytomany/')
|
||||
# response = self.list_view(request)
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# self.assertEqual(response.data, self.data)
|
||||
|
||||
# def test_get_detail_view(self):
|
||||
# """
|
||||
# GET requests to ListCreateAPIView should return list of objects.
|
||||
# """
|
||||
# request = factory.get('/manytomany/1/')
|
||||
# response = self.detail_view(request, pk=1)
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# self.assertEqual(response.data, self.data[0])
|
||||
|
||||
|
||||
# class TestHyperlinkedIdentityFieldLookup(TestCase):
|
||||
# urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
# def setUp(self):
|
||||
# """
|
||||
# Create 3 Album instances.
|
||||
# """
|
||||
# titles = ['foo', 'bar', 'baz']
|
||||
# for title in titles:
|
||||
# album = Album(title=title)
|
||||
# album.save()
|
||||
# self.detail_view = AlbumDetail.as_view()
|
||||
# self.data = {
|
||||
# 'foo': {'title': 'foo', 'url': 'http://testserver/albums/foo/'},
|
||||
# 'bar': {'title': 'bar', 'url': 'http://testserver/albums/bar/'},
|
||||
# 'baz': {'title': 'baz', 'url': 'http://testserver/albums/baz/'}
|
||||
# }
|
||||
|
||||
# def test_lookup_field(self):
|
||||
# """
|
||||
# GET requests to AlbumDetail view should return serialized Albums
|
||||
# with a url field keyed by `title`.
|
||||
# """
|
||||
# for album in Album.objects.all():
|
||||
# request = factory.get('/albums/{0}/'.format(album.title))
|
||||
# response = self.detail_view(request, title=album.title)
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# self.assertEqual(response.data, self.data[album.title])
|
||||
|
||||
|
||||
# class TestCreateWithForeignKeys(TestCase):
|
||||
# urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
# def setUp(self):
|
||||
# """
|
||||
# Create a blog post
|
||||
# """
|
||||
# self.post = BlogPost.objects.create(title="Test post")
|
||||
# self.create_view = BlogPostCommentListCreate.as_view()
|
||||
|
||||
# def test_create_comment(self):
|
||||
|
||||
# data = {
|
||||
# 'text': 'A test comment',
|
||||
# 'blog_post_url': 'http://testserver/posts/1/'
|
||||
# }
|
||||
|
||||
# request = factory.post('/comments/', data=data)
|
||||
# response = self.create_view(request)
|
||||
# self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
# self.assertEqual(response['Location'], 'http://testserver/comments/1/')
|
||||
# self.assertEqual(self.post.blogpostcomment_set.count(), 1)
|
||||
# self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment')
|
||||
|
||||
|
||||
# class TestCreateWithForeignKeysAndCustomSlug(TestCase):
|
||||
# urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
# def setUp(self):
|
||||
# """
|
||||
# Create an Album
|
||||
# """
|
||||
# self.post = Album.objects.create(title='test-album')
|
||||
# self.list_create_view = PhotoListCreate.as_view()
|
||||
|
||||
# def test_create_photo(self):
|
||||
|
||||
# data = {
|
||||
# 'description': 'A test photo',
|
||||
# 'album_url': 'http://testserver/albums/test-album/'
|
||||
# }
|
||||
|
||||
# request = factory.post('/photos/', data=data)
|
||||
# response = self.list_create_view(request)
|
||||
# self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
# self.assertNotIn('Location', response, msg='Location should only be included if there is a "url" field on the serializer')
|
||||
# self.assertEqual(self.post.photo_set.count(), 1)
|
||||
# self.assertEqual(self.post.photo_set.all()[0].description, 'A test photo')
|
||||
|
||||
|
||||
# class TestOptionalRelationHyperlinkedView(TestCase):
|
||||
# urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
# def setUp(self):
|
||||
# """
|
||||
# Create 1 OptionalRelationModel instances.
|
||||
# """
|
||||
# OptionalRelationModel().save()
|
||||
# self.objects = OptionalRelationModel.objects
|
||||
# self.detail_view = OptionalRelationDetail.as_view()
|
||||
# self.data = {"url": "http://testserver/optionalrelation/1/", "other": None}
|
||||
|
||||
# def test_get_detail_view(self):
|
||||
# """
|
||||
# GET requests to RetrieveAPIView with optional relations should return None
|
||||
# for non existing relations.
|
||||
# """
|
||||
# request = factory.get('/optionalrelationmodel-detail/1')
|
||||
# response = self.detail_view(request, pk=1)
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# self.assertEqual(response.data, self.data)
|
||||
|
||||
# def test_put_detail_view(self):
|
||||
# """
|
||||
# PUT requests to RetrieveUpdateDestroyAPIView with optional relations
|
||||
# should accept None for non existing relations.
|
||||
# """
|
||||
# response = self.client.put('/optionalrelation/1/',
|
||||
# data=json.dumps(self.data),
|
||||
# content_type='application/json')
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
||||
# class TestOverriddenURLField(TestCase):
|
||||
# def setUp(self):
|
||||
# class OverriddenURLSerializer(serializers.HyperlinkedModelSerializer):
|
||||
# url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
# class Meta:
|
||||
# model = BlogPost
|
||||
# fields = ('title', 'url')
|
||||
|
||||
# def get_url(self, obj):
|
||||
# return 'foo bar'
|
||||
|
||||
# self.Serializer = OverriddenURLSerializer
|
||||
# self.obj = BlogPost.objects.create(title='New blog post')
|
||||
|
||||
# def test_overridden_url_field(self):
|
||||
# """
|
||||
# The 'url' field should respect overriding.
|
||||
# Regression test for #936.
|
||||
# """
|
||||
# serializer = self.Serializer(self.obj)
|
||||
# self.assertEqual(
|
||||
# serializer.data,
|
||||
# {'title': 'New blog post', 'url': 'foo bar'}
|
||||
# )
|
||||
|
||||
|
||||
# class TestURLFieldNameBySettings(TestCase):
|
||||
# urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
# def setUp(self):
|
||||
# self.saved_url_field_name = api_settings.URL_FIELD_NAME
|
||||
# api_settings.URL_FIELD_NAME = 'global_url_field'
|
||||
|
||||
# class Serializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
# class Meta:
|
||||
# model = BlogPost
|
||||
# fields = ('title', api_settings.URL_FIELD_NAME)
|
||||
|
||||
# self.Serializer = Serializer
|
||||
# self.obj = BlogPost.objects.create(title="New blog post")
|
||||
|
||||
# def tearDown(self):
|
||||
# api_settings.URL_FIELD_NAME = self.saved_url_field_name
|
||||
|
||||
# def test_overridden_url_field_name(self):
|
||||
# request = factory.get('/posts/')
|
||||
# serializer = self.Serializer(self.obj, context={'request': request})
|
||||
# self.assertIn(api_settings.URL_FIELD_NAME, serializer.data)
|
||||
|
||||
|
||||
# class TestURLFieldNameByOptions(TestCase):
|
||||
# urls = 'tests.test_hyperlinkedserializers'
|
||||
|
||||
# def setUp(self):
|
||||
# class Serializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
# class Meta:
|
||||
# model = BlogPost
|
||||
# fields = ('title', 'serializer_url_field')
|
||||
# url_field_name = 'serializer_url_field'
|
||||
|
||||
# self.Serializer = Serializer
|
||||
# self.obj = BlogPost.objects.create(title="New blog post")
|
||||
|
||||
# def test_overridden_url_field_name(self):
|
||||
# request = factory.get('/posts/')
|
||||
# serializer = self.Serializer(self.obj, context={'request': request})
|
||||
# self.assertIn(self.Serializer.Meta.url_field_name, serializer.data)
|
||||
|
|
166
tests/test_metadata.py
Normal file
166
tests/test_metadata.py
Normal file
|
@ -0,0 +1,166 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import exceptions, serializers, views
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.test import APIRequestFactory
|
||||
import pytest
|
||||
|
||||
request = Request(APIRequestFactory().options('/'))
|
||||
|
||||
|
||||
class TestMetadata:
|
||||
def test_metadata(self):
|
||||
"""
|
||||
OPTIONS requests to views should return a valid 200 response.
|
||||
"""
|
||||
class ExampleView(views.APIView):
|
||||
"""Example view."""
|
||||
pass
|
||||
|
||||
response = ExampleView().options(request=request)
|
||||
expected = {
|
||||
'name': 'Example',
|
||||
'description': 'Example view.',
|
||||
'renders': [
|
||||
'application/json',
|
||||
'text/html'
|
||||
],
|
||||
'parses': [
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data'
|
||||
]
|
||||
}
|
||||
assert response.status_code == 200
|
||||
assert response.data == expected
|
||||
|
||||
def test_none_metadata(self):
|
||||
"""
|
||||
OPTIONS requests to views where `metadata_class = None` should raise
|
||||
a MethodNotAllowed exception, which will result in an HTTP 405 response.
|
||||
"""
|
||||
class ExampleView(views.APIView):
|
||||
metadata_class = None
|
||||
|
||||
with pytest.raises(exceptions.MethodNotAllowed):
|
||||
ExampleView().options(request=request)
|
||||
|
||||
def test_actions(self):
|
||||
"""
|
||||
On generic views OPTIONS should return an 'actions' key with metadata
|
||||
on the fields that may be supplied to PUT and POST requests.
|
||||
"""
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
|
||||
integer_field = serializers.IntegerField(max_value=10)
|
||||
char_field = serializers.CharField(required=False)
|
||||
|
||||
class ExampleView(views.APIView):
|
||||
"""Example view."""
|
||||
def post(self, request):
|
||||
pass
|
||||
|
||||
def get_serializer(self):
|
||||
return ExampleSerializer()
|
||||
|
||||
response = ExampleView().options(request=request)
|
||||
expected = {
|
||||
'name': 'Example',
|
||||
'description': 'Example view.',
|
||||
'renders': [
|
||||
'application/json',
|
||||
'text/html'
|
||||
],
|
||||
'parses': [
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data'
|
||||
],
|
||||
'actions': {
|
||||
'POST': {
|
||||
'choice_field': {
|
||||
'type': 'choice',
|
||||
'required': True,
|
||||
'read_only': False,
|
||||
'label': 'Choice field',
|
||||
'choices': [
|
||||
{'display_name': 'red', 'value': 'red'},
|
||||
{'display_name': 'green', 'value': 'green'},
|
||||
{'display_name': 'blue', 'value': 'blue'}
|
||||
]
|
||||
},
|
||||
'integer_field': {
|
||||
'type': 'integer',
|
||||
'required': True,
|
||||
'read_only': False,
|
||||
'label': 'Integer field'
|
||||
},
|
||||
'char_field': {
|
||||
'type': 'string',
|
||||
'required': False,
|
||||
'read_only': False,
|
||||
'label': 'Char field'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert response.status_code == 200
|
||||
assert response.data == expected
|
||||
|
||||
def test_global_permissions(self):
|
||||
"""
|
||||
If a user does not have global permissions on an action, then any
|
||||
metadata associated with it should not be included in OPTION responses.
|
||||
"""
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
|
||||
integer_field = serializers.IntegerField(max_value=10)
|
||||
char_field = serializers.CharField(required=False)
|
||||
|
||||
class ExampleView(views.APIView):
|
||||
"""Example view."""
|
||||
def post(self, request):
|
||||
pass
|
||||
|
||||
def put(self, request):
|
||||
pass
|
||||
|
||||
def get_serializer(self):
|
||||
return ExampleSerializer()
|
||||
|
||||
def check_permissions(self, request):
|
||||
if request.method == 'POST':
|
||||
raise exceptions.PermissionDenied()
|
||||
|
||||
response = ExampleView().options(request=request)
|
||||
assert response.status_code == 200
|
||||
assert list(response.data['actions'].keys()) == ['PUT']
|
||||
|
||||
def test_object_permissions(self):
|
||||
"""
|
||||
If a user does not have object permissions on an action, then any
|
||||
metadata associated with it should not be included in OPTION responses.
|
||||
"""
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
|
||||
integer_field = serializers.IntegerField(max_value=10)
|
||||
char_field = serializers.CharField(required=False)
|
||||
|
||||
class ExampleView(views.APIView):
|
||||
"""Example view."""
|
||||
def post(self, request):
|
||||
pass
|
||||
|
||||
def put(self, request):
|
||||
pass
|
||||
|
||||
def get_serializer(self):
|
||||
return ExampleSerializer()
|
||||
|
||||
def get_object(self):
|
||||
if self.request.method == 'PUT':
|
||||
raise exceptions.PermissionDenied()
|
||||
|
||||
response = ExampleView().options(request=request)
|
||||
assert response.status_code == 200
|
||||
assert list(response.data['actions'].keys()) == ['POST']
|
537
tests/test_model_serializer.py
Normal file
537
tests/test_model_serializer.py
Normal file
|
@ -0,0 +1,537 @@
|
|||
"""
|
||||
The `ModelSerializer` and `HyperlinkedModelSerializer` classes are essentially
|
||||
shortcuts for automatically creating serializers based on a given model class.
|
||||
|
||||
These tests deal with ensuring that we correctly map the model fields onto
|
||||
an appropriate set of serializer fields for each case.
|
||||
"""
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
def dedent(blocktext):
|
||||
return '\n'.join([line[12:] for line in blocktext.splitlines()[1:-1]])
|
||||
|
||||
|
||||
# Tests for regular field mappings.
|
||||
# ---------------------------------
|
||||
|
||||
class CustomField(models.Field):
|
||||
"""
|
||||
A custom model field simply for testing purposes.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class RegularFieldsModel(models.Model):
|
||||
"""
|
||||
A model class for testing regular flat fields.
|
||||
"""
|
||||
auto_field = models.AutoField(primary_key=True)
|
||||
big_integer_field = models.BigIntegerField()
|
||||
boolean_field = models.BooleanField(default=False)
|
||||
char_field = models.CharField(max_length=100)
|
||||
comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=100)
|
||||
date_field = models.DateField()
|
||||
datetime_field = models.DateTimeField()
|
||||
decimal_field = models.DecimalField(max_digits=3, decimal_places=1)
|
||||
email_field = models.EmailField(max_length=100)
|
||||
float_field = models.FloatField()
|
||||
integer_field = models.IntegerField()
|
||||
null_boolean_field = models.NullBooleanField()
|
||||
positive_integer_field = models.PositiveIntegerField()
|
||||
positive_small_integer_field = models.PositiveSmallIntegerField()
|
||||
slug_field = models.SlugField(max_length=100)
|
||||
small_integer_field = models.SmallIntegerField()
|
||||
text_field = models.TextField()
|
||||
time_field = models.TimeField()
|
||||
url_field = models.URLField(max_length=100)
|
||||
custom_field = CustomField()
|
||||
|
||||
def method(self):
|
||||
return 'method'
|
||||
|
||||
|
||||
COLOR_CHOICES = (('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))
|
||||
|
||||
|
||||
class FieldOptionsModel(models.Model):
|
||||
value_limit_field = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(10)])
|
||||
length_limit_field = models.CharField(validators=[MinLengthValidator(3)], max_length=12)
|
||||
blank_field = models.CharField(blank=True, max_length=10)
|
||||
null_field = models.IntegerField(null=True)
|
||||
default_field = models.IntegerField(default=0)
|
||||
descriptive_field = models.IntegerField(help_text='Some help text', verbose_name='A label')
|
||||
choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES)
|
||||
|
||||
|
||||
class TestRegularFieldMappings(TestCase):
|
||||
def test_regular_fields(self):
|
||||
"""
|
||||
Model fields should map to their equivelent serializer fields.
|
||||
"""
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RegularFieldsModel
|
||||
|
||||
expected = dedent("""
|
||||
TestSerializer():
|
||||
auto_field = IntegerField(read_only=True)
|
||||
big_integer_field = IntegerField()
|
||||
boolean_field = BooleanField(required=False)
|
||||
char_field = CharField(max_length=100)
|
||||
comma_separated_integer_field = CharField(max_length=100, validators=[<django.core.validators.RegexValidator object>])
|
||||
date_field = DateField()
|
||||
datetime_field = DateTimeField()
|
||||
decimal_field = DecimalField(decimal_places=1, max_digits=3)
|
||||
email_field = EmailField(max_length=100)
|
||||
float_field = FloatField()
|
||||
integer_field = IntegerField()
|
||||
null_boolean_field = NullBooleanField()
|
||||
positive_integer_field = IntegerField()
|
||||
positive_small_integer_field = IntegerField()
|
||||
slug_field = SlugField(max_length=100)
|
||||
small_integer_field = IntegerField()
|
||||
text_field = CharField(style={'type': 'textarea'})
|
||||
time_field = TimeField()
|
||||
url_field = URLField(max_length=100)
|
||||
custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>)
|
||||
""")
|
||||
self.assertEqual(repr(TestSerializer()), expected)
|
||||
|
||||
def test_field_options(self):
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FieldOptionsModel
|
||||
|
||||
expected = dedent("""
|
||||
TestSerializer():
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
value_limit_field = IntegerField(max_value=10, min_value=1)
|
||||
length_limit_field = CharField(max_length=12, min_length=3)
|
||||
blank_field = CharField(allow_blank=True, max_length=10)
|
||||
null_field = IntegerField(allow_null=True)
|
||||
default_field = IntegerField(required=False)
|
||||
descriptive_field = IntegerField(help_text='Some help text', label='A label')
|
||||
choices_field = ChoiceField(choices=[('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')])
|
||||
""")
|
||||
self.assertEqual(repr(TestSerializer()), expected)
|
||||
|
||||
def test_method_field(self):
|
||||
"""
|
||||
Properties and methods on the model should be allowed as `Meta.fields`
|
||||
values, and should map to `ReadOnlyField`.
|
||||
"""
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RegularFieldsModel
|
||||
fields = ('auto_field', 'method')
|
||||
|
||||
expected = dedent("""
|
||||
TestSerializer():
|
||||
auto_field = IntegerField(read_only=True)
|
||||
method = ReadOnlyField()
|
||||
""")
|
||||
self.assertEqual(repr(TestSerializer()), expected)
|
||||
|
||||
def test_pk_fields(self):
|
||||
"""
|
||||
Both `pk` and the actual primary key name are valid in `Meta.fields`.
|
||||
"""
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RegularFieldsModel
|
||||
fields = ('pk', 'auto_field')
|
||||
|
||||
expected = dedent("""
|
||||
TestSerializer():
|
||||
pk = IntegerField(label='Auto field', read_only=True)
|
||||
auto_field = IntegerField(read_only=True)
|
||||
""")
|
||||
self.assertEqual(repr(TestSerializer()), expected)
|
||||
|
||||
def test_extra_field_kwargs(self):
|
||||
"""
|
||||
Ensure `extra_kwargs` are passed to generated fields.
|
||||
"""
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RegularFieldsModel
|
||||
fields = ('auto_field', 'char_field')
|
||||
extra_kwargs = {'char_field': {'default': 'extra'}}
|
||||
|
||||
expected = dedent("""
|
||||
TestSerializer():
|
||||
auto_field = IntegerField(read_only=True)
|
||||
char_field = CharField(default='extra', max_length=100)
|
||||
""")
|
||||
self.assertEqual(repr(TestSerializer()), expected)
|
||||
|
||||
def test_invalid_field(self):
|
||||
"""
|
||||
Field names that do not map to a model field or relationship should
|
||||
raise a configuration errror.
|
||||
"""
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RegularFieldsModel
|
||||
fields = ('auto_field', 'invalid')
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured) as excinfo:
|
||||
TestSerializer().fields
|
||||
expected = 'Field name `invalid` is not valid for model `ModelBase`.'
|
||||
assert str(excinfo.exception) == expected
|
||||
|
||||
def test_missing_field(self):
|
||||
"""
|
||||
Fields that have been declared on the serializer class must be included
|
||||
in the `Meta.fields` if it exists.
|
||||
"""
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
missing = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = RegularFieldsModel
|
||||
fields = ('auto_field',)
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured) as excinfo:
|
||||
TestSerializer().fields
|
||||
expected = (
|
||||
'Field `missing` has been declared on serializer '
|
||||
'`TestSerializer`, but is missing from `Meta.fields`.'
|
||||
)
|
||||
assert str(excinfo.exception) == expected
|
||||
|
||||
|
||||
# Tests for relational field mappings.
|
||||
# ------------------------------------
|
||||
|
||||
class ForeignKeyTargetModel(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class ManyToManyTargetModel(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class OneToOneTargetModel(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class ThroughTargetModel(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class Supplementary(models.Model):
|
||||
extra = models.IntegerField()
|
||||
forwards = models.ForeignKey('ThroughTargetModel')
|
||||
backwards = models.ForeignKey('RelationalModel')
|
||||
|
||||
|
||||
class RelationalModel(models.Model):
|
||||
foreign_key = models.ForeignKey(ForeignKeyTargetModel, related_name='reverse_foreign_key')
|
||||
many_to_many = models.ManyToManyField(ManyToManyTargetModel, related_name='reverse_many_to_many')
|
||||
one_to_one = models.OneToOneField(OneToOneTargetModel, related_name='reverse_one_to_one')
|
||||
through = models.ManyToManyField(ThroughTargetModel, through=Supplementary, related_name='reverse_through')
|
||||
|
||||
|
||||
class TestRelationalFieldMappings(TestCase):
|
||||
def test_pk_relations(self):
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RelationalModel
|
||||
|
||||
expected = dedent("""
|
||||
TestSerializer():
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
foreign_key = PrimaryKeyRelatedField(queryset=ForeignKeyTargetModel.objects.all())
|
||||
one_to_one = PrimaryKeyRelatedField(queryset=OneToOneTargetModel.objects.all(), validators=[<UniqueValidator(queryset=RelationalModel.objects.all())>])
|
||||
many_to_many = PrimaryKeyRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all())
|
||||
through = PrimaryKeyRelatedField(many=True, read_only=True)
|
||||
""")
|
||||
self.assertEqual(repr(TestSerializer()), expected)
|
||||
|
||||
def test_nested_relations(self):
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RelationalModel
|
||||
depth = 1
|
||||
|
||||
expected = dedent("""
|
||||
TestSerializer():
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
foreign_key = NestedSerializer(read_only=True):
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
name = CharField(max_length=100)
|
||||
one_to_one = NestedSerializer(read_only=True):
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
name = CharField(max_length=100)
|
||||
many_to_many = NestedSerializer(many=True, read_only=True):
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
name = CharField(max_length=100)
|
||||
through = NestedSerializer(many=True, read_only=True):
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
name = CharField(max_length=100)
|
||||
""")
|
||||
self.assertEqual(repr(TestSerializer()), expected)
|
||||
|
||||
def test_hyperlinked_relations(self):
|
||||
class TestSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = RelationalModel
|
||||
|
||||
expected = dedent("""
|
||||
TestSerializer():
|
||||
url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
|
||||
foreign_key = HyperlinkedRelatedField(queryset=ForeignKeyTargetModel.objects.all(), view_name='foreignkeytargetmodel-detail')
|
||||
one_to_one = HyperlinkedRelatedField(queryset=OneToOneTargetModel.objects.all(), validators=[<UniqueValidator(queryset=RelationalModel.objects.all())>], view_name='onetoonetargetmodel-detail')
|
||||
many_to_many = HyperlinkedRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail')
|
||||
through = HyperlinkedRelatedField(many=True, read_only=True, view_name='throughtargetmodel-detail')
|
||||
""")
|
||||
self.assertEqual(repr(TestSerializer()), expected)
|
||||
|
||||
def test_nested_hyperlinked_relations(self):
|
||||
class TestSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = RelationalModel
|
||||
depth = 1
|
||||
|
||||
expected = dedent("""
|
||||
TestSerializer():
|
||||
url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
|
||||
foreign_key = NestedSerializer(read_only=True):
|
||||
url = HyperlinkedIdentityField(view_name='foreignkeytargetmodel-detail')
|
||||
name = CharField(max_length=100)
|
||||
one_to_one = NestedSerializer(read_only=True):
|
||||
url = HyperlinkedIdentityField(view_name='onetoonetargetmodel-detail')
|
||||
name = CharField(max_length=100)
|
||||
many_to_many = NestedSerializer(many=True, read_only=True):
|
||||
url = HyperlinkedIdentityField(view_name='manytomanytargetmodel-detail')
|
||||
name = CharField(max_length=100)
|
||||
through = NestedSerializer(many=True, read_only=True):
|
||||
url = HyperlinkedIdentityField(view_name='throughtargetmodel-detail')
|
||||
name = CharField(max_length=100)
|
||||
""")
|
||||
self.assertEqual(repr(TestSerializer()), expected)
|
||||
|
||||
def test_pk_reverse_foreign_key(self):
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ForeignKeyTargetModel
|
||||
fields = ('id', 'name', 'reverse_foreign_key')
|
||||
|
||||
expected = dedent("""
|
||||
TestSerializer():
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
name = CharField(max_length=100)
|
||||
reverse_foreign_key = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
|
||||
""")
|
||||
self.assertEqual(repr(TestSerializer()), expected)
|
||||
|
||||
def test_pk_reverse_one_to_one(self):
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = OneToOneTargetModel
|
||||
fields = ('id', 'name', 'reverse_one_to_one')
|
||||
|
||||
expected = dedent("""
|
||||
TestSerializer():
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
name = CharField(max_length=100)
|
||||
reverse_one_to_one = PrimaryKeyRelatedField(queryset=RelationalModel.objects.all())
|
||||
""")
|
||||
self.assertEqual(repr(TestSerializer()), expected)
|
||||
|
||||
def test_pk_reverse_many_to_many(self):
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ManyToManyTargetModel
|
||||
fields = ('id', 'name', 'reverse_many_to_many')
|
||||
|
||||
expected = dedent("""
|
||||
TestSerializer():
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
name = CharField(max_length=100)
|
||||
reverse_many_to_many = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
|
||||
""")
|
||||
self.assertEqual(repr(TestSerializer()), expected)
|
||||
|
||||
def test_pk_reverse_through(self):
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ThroughTargetModel
|
||||
fields = ('id', 'name', 'reverse_through')
|
||||
|
||||
expected = dedent("""
|
||||
TestSerializer():
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
name = CharField(max_length=100)
|
||||
reverse_through = PrimaryKeyRelatedField(many=True, read_only=True)
|
||||
""")
|
||||
self.assertEqual(repr(TestSerializer()), expected)
|
||||
|
||||
|
||||
class TestIntegration(TestCase):
|
||||
def setUp(self):
|
||||
self.foreign_key_target = ForeignKeyTargetModel.objects.create(
|
||||
name='foreign_key'
|
||||
)
|
||||
self.one_to_one_target = OneToOneTargetModel.objects.create(
|
||||
name='one_to_one'
|
||||
)
|
||||
self.many_to_many_targets = [
|
||||
ManyToManyTargetModel.objects.create(
|
||||
name='many_to_many (%d)' % idx
|
||||
) for idx in range(3)
|
||||
]
|
||||
self.instance = RelationalModel.objects.create(
|
||||
foreign_key=self.foreign_key_target,
|
||||
one_to_one=self.one_to_one_target,
|
||||
)
|
||||
self.instance.many_to_many = self.many_to_many_targets
|
||||
self.instance.save()
|
||||
|
||||
def test_pk_retrival(self):
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RelationalModel
|
||||
|
||||
serializer = TestSerializer(self.instance)
|
||||
expected = {
|
||||
'id': self.instance.pk,
|
||||
'foreign_key': self.foreign_key_target.pk,
|
||||
'one_to_one': self.one_to_one_target.pk,
|
||||
'many_to_many': [item.pk for item in self.many_to_many_targets],
|
||||
'through': []
|
||||
}
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_pk_create(self):
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RelationalModel
|
||||
|
||||
new_foreign_key = ForeignKeyTargetModel.objects.create(
|
||||
name='foreign_key'
|
||||
)
|
||||
new_one_to_one = OneToOneTargetModel.objects.create(
|
||||
name='one_to_one'
|
||||
)
|
||||
new_many_to_many = [
|
||||
ManyToManyTargetModel.objects.create(
|
||||
name='new many_to_many (%d)' % idx
|
||||
) for idx in range(3)
|
||||
]
|
||||
data = {
|
||||
'foreign_key': new_foreign_key.pk,
|
||||
'one_to_one': new_one_to_one.pk,
|
||||
'many_to_many': [item.pk for item in new_many_to_many],
|
||||
}
|
||||
|
||||
# Serializer should validate okay.
|
||||
serializer = TestSerializer(data=data)
|
||||
assert serializer.is_valid()
|
||||
|
||||
# Creating the instance, relationship attributes should be set.
|
||||
instance = serializer.save()
|
||||
assert instance.foreign_key.pk == new_foreign_key.pk
|
||||
assert instance.one_to_one.pk == new_one_to_one.pk
|
||||
assert [
|
||||
item.pk for item in instance.many_to_many.all()
|
||||
] == [
|
||||
item.pk for item in new_many_to_many
|
||||
]
|
||||
assert list(instance.through.all()) == []
|
||||
|
||||
# Representation should be correct.
|
||||
expected = {
|
||||
'id': instance.pk,
|
||||
'foreign_key': new_foreign_key.pk,
|
||||
'one_to_one': new_one_to_one.pk,
|
||||
'many_to_many': [item.pk for item in new_many_to_many],
|
||||
'through': []
|
||||
}
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_pk_update(self):
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RelationalModel
|
||||
|
||||
new_foreign_key = ForeignKeyTargetModel.objects.create(
|
||||
name='foreign_key'
|
||||
)
|
||||
new_one_to_one = OneToOneTargetModel.objects.create(
|
||||
name='one_to_one'
|
||||
)
|
||||
new_many_to_many = [
|
||||
ManyToManyTargetModel.objects.create(
|
||||
name='new many_to_many (%d)' % idx
|
||||
) for idx in range(3)
|
||||
]
|
||||
data = {
|
||||
'foreign_key': new_foreign_key.pk,
|
||||
'one_to_one': new_one_to_one.pk,
|
||||
'many_to_many': [item.pk for item in new_many_to_many],
|
||||
}
|
||||
|
||||
# Serializer should validate okay.
|
||||
serializer = TestSerializer(self.instance, data=data)
|
||||
assert serializer.is_valid()
|
||||
|
||||
# Creating the instance, relationship attributes should be set.
|
||||
instance = serializer.save()
|
||||
assert instance.foreign_key.pk == new_foreign_key.pk
|
||||
assert instance.one_to_one.pk == new_one_to_one.pk
|
||||
assert [
|
||||
item.pk for item in instance.many_to_many.all()
|
||||
] == [
|
||||
item.pk for item in new_many_to_many
|
||||
]
|
||||
assert list(instance.through.all()) == []
|
||||
|
||||
# Representation should be correct.
|
||||
expected = {
|
||||
'id': self.instance.pk,
|
||||
'foreign_key': new_foreign_key.pk,
|
||||
'one_to_one': new_one_to_one.pk,
|
||||
'many_to_many': [item.pk for item in new_many_to_many],
|
||||
'through': []
|
||||
}
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
|
||||
# Tests for bulk create using `ListSerializer`.
|
||||
|
||||
class BulkCreateModel(models.Model):
|
||||
name = models.CharField(max_length=10)
|
||||
|
||||
|
||||
class TestBulkCreate(TestCase):
|
||||
def test_bulk_create(self):
|
||||
class BasicModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BulkCreateModel
|
||||
fields = ('name',)
|
||||
|
||||
class BulkCreateSerializer(serializers.ListSerializer):
|
||||
child = BasicModelSerializer()
|
||||
|
||||
data = [{'name': 'a'}, {'name': 'b'}, {'name': 'c'}]
|
||||
serializer = BulkCreateSerializer(data=data)
|
||||
assert serializer.is_valid()
|
||||
|
||||
# Objects are returned by save().
|
||||
instances = serializer.save()
|
||||
assert len(instances) == 3
|
||||
assert [item.name for item in instances] == ['a', 'b', 'c']
|
||||
|
||||
# Objects have been created in the database.
|
||||
assert BulkCreateModel.objects.count() == 3
|
||||
assert list(BulkCreateModel.objects.values_list('name', flat=True)) == ['a', 'b', 'c']
|
||||
|
||||
# Serializer returns correct data.
|
||||
assert serializer.data == data
|
|
@ -1,30 +1,39 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
# from django.core.urlresolvers import reverse
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
from rest_framework.test import APITestCase
|
||||
from tests.models import NullableForeignKeySource
|
||||
from tests.serializers import NullableFKSourceSerializer
|
||||
from tests.views import NullableFKSourceDetail
|
||||
# from django.conf.urls import patterns, url
|
||||
# from rest_framework import serializers, generics
|
||||
# from rest_framework.test import APITestCase
|
||||
# from tests.models import NullableForeignKeySource
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^objects/(?P<pk>\d+)/$', NullableFKSourceDetail.as_view(), name='object-detail'),
|
||||
)
|
||||
# class NullableFKSourceSerializer(serializers.ModelSerializer):
|
||||
# class Meta:
|
||||
# model = NullableForeignKeySource
|
||||
|
||||
|
||||
class NullableForeignKeyTests(APITestCase):
|
||||
"""
|
||||
DRF should be able to handle nullable foreign keys when a test
|
||||
Client POST/PUT request is made with its own serialized object.
|
||||
"""
|
||||
urls = 'tests.test_nullable_fields'
|
||||
# class NullableFKSourceDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
# queryset = NullableForeignKeySource.objects.all()
|
||||
# serializer_class = NullableFKSourceSerializer
|
||||
|
||||
def test_updating_object_with_null_fk(self):
|
||||
obj = NullableForeignKeySource(name='example', target=None)
|
||||
obj.save()
|
||||
serialized_data = NullableFKSourceSerializer(obj).data
|
||||
|
||||
response = self.client.put(reverse('object-detail', args=[obj.pk]), serialized_data)
|
||||
# urlpatterns = patterns(
|
||||
# '',
|
||||
# url(r'^objects/(?P<pk>\d+)/$', NullableFKSourceDetail.as_view(), name='object-detail'),
|
||||
# )
|
||||
|
||||
self.assertEqual(response.data, serialized_data)
|
||||
|
||||
# class NullableForeignKeyTests(APITestCase):
|
||||
# """
|
||||
# DRF should be able to handle nullable foreign keys when a test
|
||||
# Client POST/PUT request is made with its own serialized object.
|
||||
# """
|
||||
# urls = 'tests.test_nullable_fields'
|
||||
|
||||
# def test_updating_object_with_null_fk(self):
|
||||
# obj = NullableForeignKeySource(name='example', target=None)
|
||||
# obj.save()
|
||||
# serialized_data = NullableFKSourceSerializer(obj).data
|
||||
|
||||
# response = self.client.put(reverse('object-detail', args=[obj.pk]), serialized_data)
|
||||
|
||||
# self.assertEqual(response.data, serialized_data)
|
||||
|
|
|
@ -4,7 +4,7 @@ from decimal import Decimal
|
|||
from django.core.paginator import Paginator
|
||||
from django.test import TestCase
|
||||
from django.utils import unittest
|
||||
from rest_framework import generics, status, pagination, filters, serializers
|
||||
from rest_framework import generics, serializers, status, pagination, filters
|
||||
from rest_framework.compat import django_filters
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from .models import BasicModel, FilterableItem
|
||||
|
@ -22,11 +22,22 @@ def split_arguments_from_url(url):
|
|||
return path, args
|
||||
|
||||
|
||||
class BasicSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BasicModel
|
||||
|
||||
|
||||
class FilterableItemSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FilterableItem
|
||||
|
||||
|
||||
class RootView(generics.ListCreateAPIView):
|
||||
"""
|
||||
Example description for OPTIONS.
|
||||
"""
|
||||
model = BasicModel
|
||||
queryset = BasicModel.objects.all()
|
||||
serializer_class = BasicSerializer
|
||||
paginate_by = 10
|
||||
|
||||
|
||||
|
@ -34,14 +45,16 @@ class DefaultPageSizeKwargView(generics.ListAPIView):
|
|||
"""
|
||||
View for testing default paginate_by_param usage
|
||||
"""
|
||||
model = BasicModel
|
||||
queryset = BasicModel.objects.all()
|
||||
serializer_class = BasicSerializer
|
||||
|
||||
|
||||
class PaginateByParamView(generics.ListAPIView):
|
||||
"""
|
||||
View for testing custom paginate_by_param usage
|
||||
"""
|
||||
model = BasicModel
|
||||
queryset = BasicModel.objects.all()
|
||||
serializer_class = BasicSerializer
|
||||
paginate_by_param = 'page_size'
|
||||
|
||||
|
||||
|
@ -49,7 +62,8 @@ class MaxPaginateByView(generics.ListAPIView):
|
|||
"""
|
||||
View for testing custom max_paginate_by usage
|
||||
"""
|
||||
model = BasicModel
|
||||
queryset = BasicModel.objects.all()
|
||||
serializer_class = BasicSerializer
|
||||
paginate_by = 3
|
||||
max_paginate_by = 5
|
||||
paginate_by_param = 'page_size'
|
||||
|
@ -121,7 +135,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
|
||||
self.objects = FilterableItem.objects
|
||||
self.data = [
|
||||
{'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
|
||||
{'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()}
|
||||
for obj in self.objects.all()
|
||||
]
|
||||
|
||||
|
@ -140,7 +154,8 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
fields = ['text', 'decimal', 'date']
|
||||
|
||||
class FilterFieldsRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
queryset = FilterableItem.objects.all()
|
||||
serializer_class = FilterableItemSerializer
|
||||
paginate_by = 10
|
||||
filter_class = DecimalFilter
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
|
@ -188,7 +203,8 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
return queryset.filter(decimal__lt=Decimal(request.GET['decimal']))
|
||||
|
||||
class BasicFilterFieldsRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
queryset = FilterableItem.objects.all()
|
||||
serializer_class = FilterableItemSerializer
|
||||
paginate_by = 10
|
||||
filter_backends = (DecimalFilterBackend,)
|
||||
|
||||
|
@ -365,7 +381,7 @@ class TestMaxPaginateByParam(TestCase):
|
|||
|
||||
# Tests for context in pagination serializers
|
||||
|
||||
class CustomField(serializers.Field):
|
||||
class CustomField(serializers.ReadOnlyField):
|
||||
def to_native(self, value):
|
||||
if 'view' not in self.context:
|
||||
raise RuntimeError("context isn't getting passed into custom field")
|
||||
|
@ -375,10 +391,10 @@ class CustomField(serializers.Field):
|
|||
class BasicModelSerializer(serializers.Serializer):
|
||||
text = CustomField()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BasicModelSerializer, self).__init__(*args, **kwargs)
|
||||
def to_native(self, value):
|
||||
if 'view' not in self.context:
|
||||
raise RuntimeError("context isn't getting passed into serializer init")
|
||||
raise RuntimeError("context isn't getting passed into serializer")
|
||||
return super(BasicSerializer, self).to_native(value)
|
||||
|
||||
|
||||
class TestContextPassedToCustomField(TestCase):
|
||||
|
@ -387,7 +403,7 @@ class TestContextPassedToCustomField(TestCase):
|
|||
|
||||
def test_with_pagination(self):
|
||||
class ListView(generics.ListCreateAPIView):
|
||||
model = BasicModel
|
||||
queryset = BasicModel.objects.all()
|
||||
serializer_class = BasicModelSerializer
|
||||
paginate_by = 1
|
||||
|
||||
|
@ -407,7 +423,7 @@ class LinksSerializer(serializers.Serializer):
|
|||
|
||||
class CustomPaginationSerializer(pagination.BasePaginationSerializer):
|
||||
links = LinksSerializer(source='*') # Takes the page object as the source
|
||||
total_results = serializers.Field(source='paginator.count')
|
||||
total_results = serializers.ReadOnlyField(source='paginator.count')
|
||||
|
||||
results_field = 'objects'
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.contrib.auth.models import User, Permission, Group
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from django.utils import unittest
|
||||
from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING
|
||||
from rest_framework import generics, serializers, status, permissions, authentication, HTTP_HEADER_ENCODING
|
||||
from rest_framework.compat import guardian, get_model_name
|
||||
from rest_framework.filters import DjangoObjectPermissionsFilter
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
@ -13,14 +13,21 @@ import base64
|
|||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class BasicSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BasicModel
|
||||
|
||||
|
||||
class RootView(generics.ListCreateAPIView):
|
||||
model = BasicModel
|
||||
queryset = BasicModel.objects.all()
|
||||
serializer_class = BasicSerializer
|
||||
authentication_classes = [authentication.BasicAuthentication]
|
||||
permission_classes = [permissions.DjangoModelPermissions]
|
||||
|
||||
|
||||
class InstanceView(generics.RetrieveUpdateDestroyAPIView):
|
||||
model = BasicModel
|
||||
queryset = BasicModel.objects.all()
|
||||
serializer_class = BasicSerializer
|
||||
authentication_classes = [authentication.BasicAuthentication]
|
||||
permission_classes = [permissions.DjangoModelPermissions]
|
||||
|
||||
|
@ -88,72 +95,59 @@ class ModelPermissionsIntegrationTests(TestCase):
|
|||
response = instance_view(request, pk=1)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_has_put_as_create_permissions(self):
|
||||
# User only has update permissions - should be able to update an entity.
|
||||
request = factory.put('/1', {'text': 'foobar'}, format='json',
|
||||
HTTP_AUTHORIZATION=self.updateonly_credentials)
|
||||
response = instance_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# def test_options_permitted(self):
|
||||
# request = factory.options(
|
||||
# '/',
|
||||
# HTTP_AUTHORIZATION=self.permitted_credentials
|
||||
# )
|
||||
# response = root_view(request, pk='1')
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# self.assertIn('actions', response.data)
|
||||
# self.assertEqual(list(response.data['actions'].keys()), ['POST'])
|
||||
|
||||
# But if PUTing to a new entity, permission should be denied.
|
||||
request = factory.put('/2', {'text': 'foobar'}, format='json',
|
||||
HTTP_AUTHORIZATION=self.updateonly_credentials)
|
||||
response = instance_view(request, pk='2')
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
# request = factory.options(
|
||||
# '/1',
|
||||
# HTTP_AUTHORIZATION=self.permitted_credentials
|
||||
# )
|
||||
# response = instance_view(request, pk='1')
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# self.assertIn('actions', response.data)
|
||||
# self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
|
||||
|
||||
def test_options_permitted(self):
|
||||
request = factory.options(
|
||||
'/',
|
||||
HTTP_AUTHORIZATION=self.permitted_credentials
|
||||
)
|
||||
response = root_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('actions', response.data)
|
||||
self.assertEqual(list(response.data['actions'].keys()), ['POST'])
|
||||
# def test_options_disallowed(self):
|
||||
# request = factory.options(
|
||||
# '/',
|
||||
# HTTP_AUTHORIZATION=self.disallowed_credentials
|
||||
# )
|
||||
# response = root_view(request, pk='1')
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# self.assertNotIn('actions', response.data)
|
||||
|
||||
request = factory.options(
|
||||
'/1',
|
||||
HTTP_AUTHORIZATION=self.permitted_credentials
|
||||
)
|
||||
response = instance_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('actions', response.data)
|
||||
self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
|
||||
# request = factory.options(
|
||||
# '/1',
|
||||
# HTTP_AUTHORIZATION=self.disallowed_credentials
|
||||
# )
|
||||
# response = instance_view(request, pk='1')
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# self.assertNotIn('actions', response.data)
|
||||
|
||||
def test_options_disallowed(self):
|
||||
request = factory.options(
|
||||
'/',
|
||||
HTTP_AUTHORIZATION=self.disallowed_credentials
|
||||
)
|
||||
response = root_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertNotIn('actions', response.data)
|
||||
# def test_options_updateonly(self):
|
||||
# request = factory.options(
|
||||
# '/',
|
||||
# HTTP_AUTHORIZATION=self.updateonly_credentials
|
||||
# )
|
||||
# response = root_view(request, pk='1')
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# self.assertNotIn('actions', response.data)
|
||||
|
||||
request = factory.options(
|
||||
'/1',
|
||||
HTTP_AUTHORIZATION=self.disallowed_credentials
|
||||
)
|
||||
response = instance_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertNotIn('actions', response.data)
|
||||
|
||||
def test_options_updateonly(self):
|
||||
request = factory.options(
|
||||
'/',
|
||||
HTTP_AUTHORIZATION=self.updateonly_credentials
|
||||
)
|
||||
response = root_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertNotIn('actions', response.data)
|
||||
|
||||
request = factory.options(
|
||||
'/1',
|
||||
HTTP_AUTHORIZATION=self.updateonly_credentials
|
||||
)
|
||||
response = instance_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('actions', response.data)
|
||||
self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
|
||||
# request = factory.options(
|
||||
# '/1',
|
||||
# HTTP_AUTHORIZATION=self.updateonly_credentials
|
||||
# )
|
||||
# response = instance_view(request, pk='1')
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# self.assertIn('actions', response.data)
|
||||
# self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
|
||||
|
||||
|
||||
class BasicPermModel(models.Model):
|
||||
|
@ -167,6 +161,11 @@ class BasicPermModel(models.Model):
|
|||
)
|
||||
|
||||
|
||||
class BasicPermSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BasicPermModel
|
||||
|
||||
|
||||
# Custom object-level permission, that includes 'view' permissions
|
||||
class ViewObjectPermissions(permissions.DjangoObjectPermissions):
|
||||
perms_map = {
|
||||
|
@ -181,7 +180,8 @@ class ViewObjectPermissions(permissions.DjangoObjectPermissions):
|
|||
|
||||
|
||||
class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
|
||||
model = BasicPermModel
|
||||
queryset = BasicPermModel.objects.all()
|
||||
serializer_class = BasicPermSerializer
|
||||
authentication_classes = [authentication.BasicAuthentication]
|
||||
permission_classes = [ViewObjectPermissions]
|
||||
|
||||
|
@ -189,7 +189,8 @@ object_permissions_view = ObjectPermissionInstanceView.as_view()
|
|||
|
||||
|
||||
class ObjectPermissionListView(generics.ListAPIView):
|
||||
model = BasicPermModel
|
||||
queryset = BasicPermModel.objects.all()
|
||||
serializer_class = BasicPermSerializer
|
||||
authentication_classes = [authentication.BasicAuthentication]
|
||||
permission_classes = [ViewObjectPermissions]
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user