Commit b106ebd2 by Tom Christie

Merge pull request #1800 from tomchristie/version-3.0

Version 3.0
parents 650a91ac 8861a7df
...@@ -75,7 +75,7 @@ You can also use the excellent [`tox`][tox] testing tool to run the tests agains ...@@ -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 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. 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]. ...@@ -27,7 +27,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements # Requirements
* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) * 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 # Installation
......
...@@ -274,7 +274,27 @@ Corresponds to `django.db.models.fields.FloatField`. ...@@ -274,7 +274,27 @@ Corresponds to `django.db.models.fields.FloatField`.
## DecimalField ## 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`. Corresponds to `django.db.models.fields.DecimalField`.
......
...@@ -212,8 +212,6 @@ Provides a `.list(request, *args, **kwargs)` method, that implements listing a q ...@@ -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 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 ## CreateModelMixin
Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance. 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 ...@@ -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. 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 # Third party packages
The following third party packages provide additional generic view implementations. The following third party packages provide additional generic view implementations.
......
<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.
>
> &mdash; [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 ...@@ -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. 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`. 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` The default JSON encoding style can be altered using the `UNICODE_JSON` and `COMPACT_JSON` settings keys.
**.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.
**.media_type**: `application/json` **.media_type**: `application/json`
......
...@@ -265,7 +265,7 @@ A format string that should be used by default for rendering the output of `Date ...@@ -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. May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
Default: `None` Default: `'iso-8601'`
#### DATETIME_INPUT_FORMATS #### DATETIME_INPUT_FORMATS
...@@ -281,7 +281,7 @@ A format string that should be used by default for rendering the output of `Date ...@@ -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. May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
Default: `None` Default: `'iso-8601'`
#### DATE_INPUT_FORMATS #### DATE_INPUT_FORMATS
...@@ -297,7 +297,7 @@ A format string that should be used by default for rendering the output of `Time ...@@ -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. May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
Default: `None` Default: `'iso-8601'`
#### TIME_INPUT_FORMATS #### TIME_INPUT_FORMATS
...@@ -309,6 +309,46 @@ Default: `['iso-8601']` ...@@ -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 ## 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.** **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 ...@@ -378,4 +418,6 @@ An integer of 0 or more, that may be used to specify the number of application p
Default: `None` Default: `None`
[cite]: http://www.python.org/dev/peps/pep-0020/ [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 [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 ...@@ -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`. 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 ## Example
The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests. The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests.
......
<a class="github" href="validators.py"></a>
# Validators
> Validators can be useful for re-using validation logic between different types of fields.
>
> &mdash; [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: ...@@ -50,7 +50,7 @@ Some reasons you might want to use REST framework:
REST framework requires the following: REST framework requires the following:
* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) * 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: The following packages are optional:
...@@ -173,6 +173,7 @@ The API guide is your complete reference manual to all the functionality provide ...@@ -173,6 +173,7 @@ The API guide is your complete reference manual to all the functionality provide
* [Serializers][serializers] * [Serializers][serializers]
* [Serializer fields][fields] * [Serializer fields][fields]
* [Serializer relations][relations] * [Serializer relations][relations]
* [Validators][validators]
* [Authentication][authentication] * [Authentication][authentication]
* [Permissions][permissions] * [Permissions][permissions]
* [Throttling][throttling] * [Throttling][throttling]
...@@ -294,6 +295,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -294,6 +295,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[serializers]: api-guide/serializers.md [serializers]: api-guide/serializers.md
[fields]: api-guide/fields.md [fields]: api-guide/fields.md
[relations]: api-guide/relations.md [relations]: api-guide/relations.md
[validation]: api-guide/validation.md
[authentication]: api-guide/authentication.md [authentication]: api-guide/authentication.md
[permissions]: api-guide/permissions.md [permissions]: api-guide/permissions.md
[throttling]: api-guide/throttling.md [throttling]: api-guide/throttling.md
......
...@@ -95,6 +95,7 @@ a.fusion-poweredby { ...@@ -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/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/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/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/authentication{{ suffix }}">Authentication</a></li>
<li><a href="{{ base_url }}/api-guide/permissions{{ suffix }}">Permissions</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> <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 ...@@ -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. 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. The `DEFAULT_MODEL_SERIALIZER_CLASS` API setting is now also deprecated.
......
...@@ -109,7 +109,7 @@ You can also use the excellent [tox][tox] testing tool to run the tests against ...@@ -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 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. 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`: ...@@ -159,7 +159,7 @@ You can determine your currently installed version using `pip freeze`:
* Added `write_only_fields` option to `ModelSerializer` classes. * Added `write_only_fields` option to `ModelSerializer` classes.
* JSON renderer now deals with objects that implement a dict-like interface. * JSON renderer now deals with objects that implement a dict-like interface.
* Fix compatiblity with newer versions of `django-oauth-plus`. * 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: 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. * Bugfix: Prevent double-escaping of non-latin1 URL query params when appending `format=json` params.
......
...@@ -6,7 +6,7 @@ ...@@ -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. 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 ## 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. ...@@ -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 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"`. 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:
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`.
INSTALLED_APPS = ( INSTALLED_APPS = (
... ...
...@@ -72,7 +59,7 @@ Okay, we're ready to roll. ...@@ -72,7 +59,7 @@ Okay, we're ready to roll.
## Creating a model to work with ## 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 django.db import models
from pygments.lexers import get_all_lexers 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 ...@@ -98,9 +85,10 @@ For the purposes of this tutorial we're going to start by creating a simple `Sni
class Meta: class Meta:
ordering = ('created',) 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 ## 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 ...@@ -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): 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, title = serializers.CharField(required=False,
max_length=100) max_length=100)
code = serializers.CharField(widget=widgets.Textarea, code = serializers.CharField(style={'type': 'textarea'})
max_length=100000)
linenos = serializers.BooleanField(required=False) linenos = serializers.BooleanField(required=False)
language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, language = serializers.ChoiceField(choices=LANGUAGE_CHOICES,
default='python') default='python')
style = serializers.ChoiceField(choices=STYLE_CHOICES, style = serializers.ChoiceField(choices=STYLE_CHOICES,
default='friendly') default='friendly')
def restore_object(self, attrs, instance=None): def create(self, validated_attrs):
""" """
Create or update a new snippet instance, given a dictionary Create and return a new `Snippet` instance, given the validated data.
of deserialized field values. """
return Snippet.objects.create(**validated_attrs)
Note that if we don't define this method, then deserializing def update(self, instance, validated_attrs):
data will simply return a dictionary of items. """
Update and return an existing `Snippet` instance, given the validated data.
""" """
if instance: instance.title = validated_attrs.get('title', instance.title)
# Update existing instance instance.code = validated_attrs.get('code', instance.code)
instance.title = attrs.get('title', instance.title) instance.linenos = validated_attrs.get('linenos', instance.linenos)
instance.code = attrs.get('code', instance.code) instance.language = validated_attrs.get('language', instance.language)
instance.linenos = attrs.get('linenos', instance.linenos) instance.style = validated_attrs.get('style', instance.style)
instance.language = attrs.get('language', instance.language) instance.save()
instance.style = attrs.get('style', instance.style)
return instance return instance
# Create new instance 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()`
return Snippet(**attrs)
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. 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`.
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. 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. 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` ...@@ -219,6 +206,24 @@ Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer`
model = Snippet model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style') 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 ## Writing regular Django views using our Serializer
Let's see how we can write some API views using our new Serializer class. 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 ...@@ -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. 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): def perform_create(self, serializer):
obj.owner = self.request.user 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 ## 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`: 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. **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. 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 ## Adding required permissions to views
......
...@@ -26,11 +26,13 @@ Create a new Django project named `tutorial`, then start a new app called `quick ...@@ -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: 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 ## Serializers
......
...@@ -59,6 +59,7 @@ path_list = [ ...@@ -59,6 +59,7 @@ path_list = [
'api-guide/serializers.md', 'api-guide/serializers.md',
'api-guide/fields.md', 'api-guide/fields.md',
'api-guide/relations.md', 'api-guide/relations.md',
'api-guide/validators.md',
'api-guide/authentication.md', 'api-guide/authentication.md',
'api-guide/permissions.md', 'api-guide/permissions.md',
'api-guide/throttling.md', 'api-guide/throttling.md',
......
...@@ -8,8 +8,8 @@ flake8==2.2.2 ...@@ -8,8 +8,8 @@ flake8==2.2.2
markdown>=2.1.0 markdown>=2.1.0
PyYAML>=3.10 PyYAML>=3.10
defusedxml>=0.3 defusedxml>=0.3
django-guardian==1.2.4
django-filter>=0.5.4 django-filter>=0.5.4
django-oauth-plus>=2.2.1 django-oauth-plus>=2.2.1
oauth2>=1.5.211 oauth2>=1.5.211
django-oauth2-provider>=0.2.4 django-oauth2-provider>=0.2.4
Pillow==2.3.0
...@@ -8,7 +8,7 @@ ______ _____ _____ _____ __ ...@@ -8,7 +8,7 @@ ______ _____ _____ _____ __
""" """
__title__ = 'Django REST framework' __title__ = 'Django REST framework'
__version__ = '2.4.4' __version__ = '3.0.0'
__author__ = 'Tom Christie' __author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause' __license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2014 Tom Christie' __copyright__ = 'Copyright 2011-2014 Tom Christie'
......
...@@ -129,7 +129,7 @@ class SessionAuthentication(BaseAuthentication): ...@@ -129,7 +129,7 @@ class SessionAuthentication(BaseAuthentication):
reason = CSRFCheck().process_view(request, None, (), {}) reason = CSRFCheck().process_view(request, None, (), {})
if reason: if reason:
# CSRF failed, bail with explicit error message # CSRF failed, bail with explicit error message
raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason) raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
class TokenAuthentication(BaseAuthentication): class TokenAuthentication(BaseAuthentication):
......
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import exceptions, serializers
class AuthTokenSerializer(serializers.Serializer): class AuthTokenSerializer(serializers.Serializer):
...@@ -18,12 +18,13 @@ class AuthTokenSerializer(serializers.Serializer): ...@@ -18,12 +18,13 @@ class AuthTokenSerializer(serializers.Serializer):
if user: if user:
if not user.is_active: if not user.is_active:
msg = _('User account is disabled.') msg = _('User account is disabled.')
raise serializers.ValidationError(msg) raise exceptions.ValidationError(msg)
attrs['user'] = user
return attrs
else: else:
msg = _('Unable to log in with provided credentials.') msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg) raise exceptions.ValidationError(msg)
else: else:
msg = _('Must include "username" and "password"') 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): ...@@ -16,9 +16,10 @@ class ObtainAuthToken(APIView):
model = Token model = Token
def post(self, request): def post(self, request):
serializer = self.serializer_class(data=request.DATA) serializer = self.serializer_class(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
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({'token': token.key})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
......
...@@ -5,11 +5,12 @@ versions of django/python, and compatibility wrappers around optional packages. ...@@ -5,11 +5,12 @@ versions of django/python, and compatibility wrappers around optional packages.
# flake8: noqa # flake8: noqa
from __future__ import unicode_literals from __future__ import unicode_literals
import django
import inspect
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.conf import settings from django.conf import settings
from django.utils import six from django.utils import six
import django
import inspect
# Handle django.utils.encoding rename in 1.5 onwards. # Handle django.utils.encoding rename in 1.5 onwards.
...@@ -39,6 +40,17 @@ except ImportError: ...@@ -39,6 +40,17 @@ except ImportError:
django_filters = None 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 # Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
# Fixes (#1712). We keep the try/except for the test suite. # Fixes (#1712). We keep the try/except for the test suite.
guardian = None guardian = None
...@@ -73,15 +85,6 @@ except ImportError: ...@@ -73,15 +85,6 @@ except ImportError:
from collections import UserDict from collections import UserDict
from collections import MutableMapping as DictMixin 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): def get_model_name(model_cls):
try: try:
...@@ -110,6 +113,62 @@ else: ...@@ -110,6 +113,62 @@ else:
return [m.upper() for m in self.http_method_names if hasattr(self, m)] 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 # PATCH method is not implemented by Django
if 'patch' not in View.http_method_names: if 'patch' not in View.http_method_names:
View.http_method_names = View.http_method_names + ['patch'] View.http_method_names = View.http_method_names + ['patch']
...@@ -133,12 +192,12 @@ class RequestFactory(DjangoRequestFactory): ...@@ -133,12 +192,12 @@ class RequestFactory(DjangoRequestFactory):
r = { r = {
'PATH_INFO': self._get_path(parsed), 'PATH_INFO': self._get_path(parsed),
'QUERY_STRING': force_text(parsed[4]), 'QUERY_STRING': force_text(parsed[4]),
'REQUEST_METHOD': str(method), 'REQUEST_METHOD': six.text_type(method),
} }
if data: if data:
r.update({ r.update({
'CONTENT_LENGTH': len(data), 'CONTENT_LENGTH': len(data),
'CONTENT_TYPE': str(content_type), 'CONTENT_TYPE': six.text_type(content_type),
'wsgi.input': FakePayload(data), 'wsgi.input': FakePayload(data),
}) })
elif django.VERSION <= (1, 4): elif django.VERSION <= (1, 4):
...@@ -232,6 +291,15 @@ except ImportError: ...@@ -232,6 +291,15 @@ except ImportError:
oauth2_constants = None oauth2_constants = None
provider_now = 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 # Handle lazy strings across Py2/Py3
from django.utils.functional import Promise from django.utils.functional import Promise
......
...@@ -10,7 +10,6 @@ from __future__ import unicode_literals ...@@ -10,7 +10,6 @@ from __future__ import unicode_literals
from django.utils import six from django.utils import six
from rest_framework.views import APIView from rest_framework.views import APIView
import types import types
import warnings
def api_view(http_method_names): def api_view(http_method_names):
...@@ -130,37 +129,3 @@ def list_route(methods=['get'], **kwargs): ...@@ -130,37 +129,3 @@ def list_route(methods=['get'], **kwargs):
func.kwargs = kwargs func.kwargs = kwargs
return func return func
return decorator 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): ...@@ -15,7 +15,7 @@ class APIException(Exception):
Subclasses should provide `.status_code` and `.default_detail` properties. Subclasses should provide `.status_code` and `.default_detail` properties.
""" """
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
default_detail = '' default_detail = 'A server error occured'
def __init__(self, detail=None): def __init__(self, detail=None):
self.detail = detail or self.default_detail self.detail = detail or self.default_detail
...@@ -24,6 +24,27 @@ class APIException(Exception): ...@@ -24,6 +24,27 @@ class APIException(Exception):
return self.detail 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): class ParseError(APIException):
status_code = status.HTTP_400_BAD_REQUEST status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Malformed request.' default_detail = 'Malformed request.'
...@@ -54,7 +75,7 @@ class MethodNotAllowed(APIException): ...@@ -54,7 +75,7 @@ class MethodNotAllowed(APIException):
class NotAcceptable(APIException): class NotAcceptable(APIException):
status_code = status.HTTP_406_NOT_ACCEPTABLE 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): def __init__(self, detail=None, available_renderers=None):
self.detail = detail or self.default_detail self.detail = detail or self.default_detail
......
...@@ -3,6 +3,7 @@ Provides generic filtering backends that can be used to filter the results ...@@ -3,6 +3,7 @@ Provides generic filtering backends that can be used to filter the results
returned by list views. returned by list views.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
from django.utils import six from django.utils import six
...@@ -64,7 +65,7 @@ class DjangoFilterBackend(BaseFilterBackend): ...@@ -64,7 +65,7 @@ class DjangoFilterBackend(BaseFilterBackend):
filter_class = self.get_filter_class(view, queryset) filter_class = self.get_filter_class(view, queryset)
if filter_class: if filter_class:
return filter_class(request.QUERY_PARAMS, queryset=queryset).qs return filter_class(request.query_params, queryset=queryset).qs
return queryset return queryset
...@@ -78,7 +79,7 @@ class SearchFilter(BaseFilterBackend): ...@@ -78,7 +79,7 @@ class SearchFilter(BaseFilterBackend):
Search terms are set by a ?search=... query parameter, Search terms are set by a ?search=... query parameter,
and may be comma and/or whitespace delimited. and may be comma and/or whitespace delimited.
""" """
params = request.QUERY_PARAMS.get(self.search_param, '') params = request.query_params.get(self.search_param, '')
return params.replace(',', ' ').split() return params.replace(',', ' ').split()
def construct_search(self, field_name): def construct_search(self, field_name):
...@@ -97,7 +98,7 @@ class SearchFilter(BaseFilterBackend): ...@@ -97,7 +98,7 @@ class SearchFilter(BaseFilterBackend):
if not search_fields: if not search_fields:
return queryset 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_field in search_fields]
for search_term in self.get_search_terms(request): for search_term in self.get_search_terms(request):
...@@ -121,7 +122,7 @@ class OrderingFilter(BaseFilterBackend): ...@@ -121,7 +122,7 @@ class OrderingFilter(BaseFilterBackend):
the `ordering_param` value on the OrderingFilter or by the `ordering_param` value on the OrderingFilter or by
specifying an `ORDERING_PARAM` value in the API settings. specifying an `ORDERING_PARAM` value in the API settings.
""" """
params = request.QUERY_PARAMS.get(self.ordering_param) params = request.query_params.get(self.ordering_param)
if params: if params:
return [param.strip() for param in params.split(',')] return [param.strip() for param in params.split(',')]
...@@ -147,7 +148,7 @@ class OrderingFilter(BaseFilterBackend): ...@@ -147,7 +148,7 @@ class OrderingFilter(BaseFilterBackend):
if not getattr(field, 'write_only', False) if not getattr(field, 'write_only', False)
] ]
elif valid_fields == '__all__': 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 = [field.name for field in queryset.model._meta.fields]
valid_fields += queryset.query.aggregates.keys() valid_fields += queryset.query.aggregates.keys()
......
"""
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. ...@@ -6,40 +6,9 @@ which allows mixin classes to be composed in interesting ways.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.exceptions import ValidationError
from django.http import Http404
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.request import clone_request
from rest_framework.settings import api_settings 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): class CreateModelMixin(object):
...@@ -47,17 +16,14 @@ class CreateModelMixin(object): ...@@ -47,17 +16,14 @@ class CreateModelMixin(object):
Create a model instance. Create a model instance.
""" """
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA, files=request.FILES) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
if serializer.is_valid(): self.perform_create(serializer)
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) headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
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): def get_success_headers(self, data):
try: try:
...@@ -70,31 +36,13 @@ class ListModelMixin(object): ...@@ -70,31 +36,13 @@ class ListModelMixin(object):
""" """
List a queryset. List a queryset.
""" """
empty_error = "Empty list and '%(class_name)s.allow_empty' is False."
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
self.object_list = self.filter_queryset(self.get_queryset()) instance = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(instance)
# 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)
if page is not None: if page is not None:
serializer = self.get_pagination_serializer(page) serializer = self.get_pagination_serializer(page)
else: else:
serializer = self.get_serializer(self.object_list, many=True) serializer = self.get_serializer(instance, many=True)
return Response(serializer.data) return Response(serializer.data)
...@@ -103,8 +51,8 @@ class RetrieveModelMixin(object): ...@@ -103,8 +51,8 @@ class RetrieveModelMixin(object):
Retrieve a model instance. Retrieve a model instance.
""" """
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
self.object = self.get_object() instance = self.get_object()
serializer = self.get_serializer(self.object) serializer = self.get_serializer(instance)
return Response(serializer.data) return Response(serializer.data)
...@@ -114,83 +62,28 @@ class UpdateModelMixin(object): ...@@ -114,83 +62,28 @@ class UpdateModelMixin(object):
""" """
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False) partial = kwargs.pop('partial', False)
self.object = self.get_object_or_none() instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer = self.get_serializer(self.object, data=request.DATA, serializer.is_valid(raise_exception=True)
files=request.FILES, partial=partial) self.perform_update(serializer)
return Response(serializer.data)
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) def perform_update(self, serializer):
self.post_save(self.object, created=False) serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True kwargs['partial'] = True
return self.update(request, *args, **kwargs) 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): class DestroyModelMixin(object):
""" """
Destroy a model instance. Destroy a model instance.
""" """
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
obj = self.get_object() instance = self.get_object()
self.pre_delete(obj) self.perform_destroy(instance)
obj.delete()
self.post_delete(obj)
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
def perform_destroy(self, instance):
instance.delete()
...@@ -38,7 +38,7 @@ class DefaultContentNegotiation(BaseContentNegotiation): ...@@ -38,7 +38,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
""" """
# Allow URL style format override. eg. "?format=json # Allow URL style format override. eg. "?format=json
format_query_param = self.settings.URL_FORMAT_OVERRIDE format_query_param = self.settings.URL_FORMAT_OVERRIDE
format = format_suffix or request.QUERY_PARAMS.get(format_query_param) format = format_suffix or request.query_params.get(format_query_param)
if format: if format:
renderers = self.filter_renderers(renderers, format) renderers = self.filter_renderers(renderers, format)
...@@ -87,5 +87,5 @@ class DefaultContentNegotiation(BaseContentNegotiation): ...@@ -87,5 +87,5 @@ class DefaultContentNegotiation(BaseContentNegotiation):
Allows URL style accept override. eg. "?accept=application/json" Allows URL style accept override. eg. "?accept=application/json"
""" """
header = request.META.get('HTTP_ACCEPT', '*/*') header = request.META.get('HTTP_ACCEPT', '*/*')
header = request.QUERY_PARAMS.get(self.settings.URL_ACCEPT_OVERRIDE, header) header = request.query_params.get(self.settings.URL_ACCEPT_OVERRIDE, header)
return [token.strip() for token in header.split(',')] return [token.strip() for token in header.split(',')]
...@@ -13,7 +13,7 @@ class NextPageField(serializers.Field): ...@@ -13,7 +13,7 @@ class NextPageField(serializers.Field):
""" """
page_field = 'page' page_field = 'page'
def to_native(self, value): def to_representation(self, value):
if not value.has_next(): if not value.has_next():
return None return None
page = value.next_page_number() page = value.next_page_number()
...@@ -28,7 +28,7 @@ class PreviousPageField(serializers.Field): ...@@ -28,7 +28,7 @@ class PreviousPageField(serializers.Field):
""" """
page_field = 'page' page_field = 'page'
def to_native(self, value): def to_representation(self, value):
if not value.has_previous(): if not value.has_previous():
return None return None
page = value.previous_page_number() page = value.previous_page_number()
...@@ -37,7 +37,7 @@ class PreviousPageField(serializers.Field): ...@@ -37,7 +37,7 @@ class PreviousPageField(serializers.Field):
return replace_query_param(url, self.page_field, page) 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 If no object serializer is specified, then this serializer will be applied
as the default. as the default.
...@@ -49,25 +49,11 @@ class DefaultObjectSerializer(serializers.Field): ...@@ -49,25 +49,11 @@ class DefaultObjectSerializer(serializers.Field):
super(DefaultObjectSerializer, self).__init__(source=source) 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): class BasePaginationSerializer(serializers.Serializer):
""" """
A base class for pagination serializers to inherit from, A base class for pagination serializers to inherit from,
to make implementing custom serializers more easy. to make implementing custom serializers more easy.
""" """
_options_class = PaginationSerializerOptions
results_field = 'results' results_field = 'results'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
...@@ -76,22 +62,22 @@ class BasePaginationSerializer(serializers.Serializer): ...@@ -76,22 +62,22 @@ class BasePaginationSerializer(serializers.Serializer):
""" """
super(BasePaginationSerializer, self).__init__(*args, **kwargs) super(BasePaginationSerializer, self).__init__(*args, **kwargs)
results_field = self.results_field results_field = self.results_field
object_serializer = self.opts.object_serializer_class
if 'context' in kwargs: try:
context_kwarg = {'context': kwargs['context']} object_serializer = self.Meta.object_serializer_class
else: except AttributeError:
context_kwarg = {} object_serializer = DefaultObjectSerializer
self.fields[results_field] = object_serializer(source='object_list', self.fields[results_field] = serializers.ListSerializer(
many=True, child=object_serializer(),
**context_kwarg) source='object_list'
)
class PaginationSerializer(BasePaginationSerializer): class PaginationSerializer(BasePaginationSerializer):
""" """
A default implementation of a pagination serializer. A default implementation of a pagination serializer.
""" """
count = serializers.Field(source='paginator.count') count = serializers.ReadOnlyField(source='paginator.count')
next = NextPageField(source='*') next = NextPageField(source='*')
previous = PreviousPageField(source='*') previous = PreviousPageField(source='*')
...@@ -5,6 +5,7 @@ They give us a generic way of being able to handle various media types ...@@ -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. on the request, such as form content or json encoded data.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.core.files.uploadhandler import StopFutureHandlers from django.core.files.uploadhandler import StopFutureHandlers
from django.http import QueryDict from django.http import QueryDict
...@@ -48,7 +49,7 @@ class JSONParser(BaseParser): ...@@ -48,7 +49,7 @@ class JSONParser(BaseParser):
""" """
media_type = 'application/json' media_type = 'application/json'
renderer_class = renderers.UnicodeJSONRenderer renderer_class = renderers.JSONRenderer
def parse(self, stream, media_type=None, parser_context=None): def parse(self, stream, media_type=None, parser_context=None):
""" """
...@@ -132,7 +133,7 @@ class MultiPartParser(BaseParser): ...@@ -132,7 +133,7 @@ class MultiPartParser(BaseParser):
data, files = parser.parse() data, files = parser.parse()
return DataAndFiles(data, files) return DataAndFiles(data, files)
except MultiPartParserError as exc: 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): class XMLParser(BaseParser):
......
...@@ -4,7 +4,7 @@ The Request class is used as a wrapper around the standard request object. ...@@ -4,7 +4,7 @@ The Request class is used as a wrapper around the standard request object.
The wrapped request then offers a richer API, in particular : The wrapped request then offers a richer API, in particular :
- content automatically parsed according to `Content-Type` header, - content automatically parsed according to `Content-Type` header,
and available as `request.DATA` and available as `request.data`
- full support of PUT method, including support for file uploads - full support of PUT method, including support for file uploads
- form overloading of HTTP method, content type and content - form overloading of HTTP method, content type and content
""" """
...@@ -13,10 +13,12 @@ from django.conf import settings ...@@ -13,10 +13,12 @@ from django.conf import settings
from django.http import QueryDict from django.http import QueryDict
from django.http.multipartparser import parse_header from django.http.multipartparser import parse_header
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.datastructures import MergeDict as DjangoMergeDict
from rest_framework import HTTP_HEADER_ENCODING from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework.compat import BytesIO from rest_framework.compat import BytesIO
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
import warnings
def is_form_media_type(media_type): def is_form_media_type(media_type):
...@@ -58,6 +60,15 @@ class override_method(object): ...@@ -58,6 +60,15 @@ class override_method(object):
self.view.action = self.action self.view.action = self.action
class MergeDict(DjangoMergeDict, dict):
"""
Using this as a workaround until the parsers API is properly
addressed in 3.1.
"""
def __init__(self, *dicts):
self.dicts = dicts
class Empty(object): class Empty(object):
""" """
Placeholder for unset attributes. Placeholder for unset attributes.
...@@ -82,6 +93,7 @@ def clone_request(request, method): ...@@ -82,6 +93,7 @@ def clone_request(request, method):
parser_context=request.parser_context) parser_context=request.parser_context)
ret._data = request._data ret._data = request._data
ret._files = request._files ret._files = request._files
ret._full_data = request._full_data
ret._content_type = request._content_type ret._content_type = request._content_type
ret._stream = request._stream ret._stream = request._stream
ret._method = method ret._method = method
...@@ -133,6 +145,7 @@ class Request(object): ...@@ -133,6 +145,7 @@ class Request(object):
self.parser_context = parser_context self.parser_context = parser_context
self._data = Empty self._data = Empty
self._files = Empty self._files = Empty
self._full_data = Empty
self._method = Empty self._method = Empty
self._content_type = Empty self._content_type = Empty
self._stream = Empty self._stream = Empty
...@@ -186,13 +199,31 @@ class Request(object): ...@@ -186,13 +199,31 @@ class Request(object):
return self._stream return self._stream
@property @property
def QUERY_PARAMS(self): def query_params(self):
""" """
More semantically correct name for request.GET. More semantically correct name for request.GET.
""" """
return self._request.GET return self._request.GET
@property @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): def DATA(self):
""" """
Parses the request body and returns the data. Parses the request body and returns the data.
...@@ -200,6 +231,11 @@ class Request(object): ...@@ -200,6 +231,11 @@ class Request(object):
Similar to usual behaviour of `request.POST`, except that it handles Similar to usual behaviour of `request.POST`, except that it handles
arbitrary parsers, and also works on methods other than POST (eg PUT). 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'): if not _hasattr(self, '_data'):
self._load_data_and_files() self._load_data_and_files()
return self._data return self._data
...@@ -212,6 +248,11 @@ class Request(object): ...@@ -212,6 +248,11 @@ class Request(object):
Similar to usual behaviour of `request.FILES`, except that it handles Similar to usual behaviour of `request.FILES`, except that it handles
arbitrary parsers, and also works on methods other than POST (eg PUT). 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'): if not _hasattr(self, '_files'):
self._load_data_and_files() self._load_data_and_files()
return self._files return self._files
...@@ -272,6 +313,10 @@ class Request(object): ...@@ -272,6 +313,10 @@ class Request(object):
if not _hasattr(self, '_data'): if not _hasattr(self, '_data'):
self._data, self._files = self._parse() self._data, self._files = self._parse()
if self._files:
self._full_data = MergeDict(self._data, self._files)
else:
self._full_data = self._data
def _load_method_and_content_type(self): def _load_method_and_content_type(self):
""" """
...@@ -333,6 +378,7 @@ class Request(object): ...@@ -333,6 +378,7 @@ class Request(object):
# At this point we're committed to parsing the request as form data. # At this point we're committed to parsing the request as form data.
self._data = self._request.POST self._data = self._request.POST
self._files = self._request.FILES self._files = self._request.FILES
self._full_data = MergeDict(self._data, self._files)
# Method overloading - change the method and remove the param from the content. # Method overloading - change the method and remove the param from the content.
if ( if (
...@@ -350,7 +396,7 @@ class Request(object): ...@@ -350,7 +396,7 @@ class Request(object):
): ):
self._content_type = self._data[self._CONTENTTYPE_PARAM] self._content_type = self._data[self._CONTENTTYPE_PARAM]
self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding'])) self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))
self._data, self._files = (Empty, Empty) self._data, self._files, self._full_data = (Empty, Empty, Empty)
def _parse(self): def _parse(self):
""" """
...@@ -380,6 +426,7 @@ class Request(object): ...@@ -380,6 +426,7 @@ class Request(object):
# logging the request or similar. # logging the request or similar.
self._data = QueryDict('', encoding=self._request._encoding) self._data = QueryDict('', encoding=self._request._encoding)
self._files = MultiValueDict() self._files = MultiValueDict()
self._full_data = self._data
raise raise
# Parser classes may return the raw data, or a # Parser classes may return the raw data, or a
......
...@@ -3,6 +3,7 @@ Provide reverse functions that return fully qualified URLs ...@@ -3,6 +3,7 @@ Provide reverse functions that return fully qualified URLs
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.urlresolvers import reverse as django_reverse from django.core.urlresolvers import reverse as django_reverse
from django.utils import six
from django.utils.functional import lazy from django.utils.functional import lazy
...@@ -20,4 +21,4 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra ...@@ -20,4 +21,4 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra
return url return url
reverse_lazy = lazy(reverse, str) reverse_lazy = lazy(reverse, six.text_type)
...@@ -45,6 +45,7 @@ DEFAULTS = { ...@@ -45,6 +45,7 @@ DEFAULTS = {
), ),
'DEFAULT_THROTTLE_CLASSES': (), 'DEFAULT_THROTTLE_CLASSES': (),
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation', 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
# Genric view behavior # Genric view behavior
'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer', 'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer',
...@@ -77,6 +78,7 @@ DEFAULTS = { ...@@ -77,6 +78,7 @@ DEFAULTS = {
# Exception handling # Exception handling
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler', 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
'NON_FIELD_ERRORS_KEY': 'non_field_errors',
# Testing # Testing
'TEST_REQUEST_RENDERER_CLASSES': ( 'TEST_REQUEST_RENDERER_CLASSES': (
...@@ -96,24 +98,20 @@ DEFAULTS = { ...@@ -96,24 +98,20 @@ DEFAULTS = {
'URL_FIELD_NAME': 'url', 'URL_FIELD_NAME': 'url',
# Input and output formats # Input and output formats
'DATE_INPUT_FORMATS': ( 'DATE_FORMAT': ISO_8601,
ISO_8601, 'DATE_INPUT_FORMATS': (ISO_8601,),
),
'DATE_FORMAT': None,
'DATETIME_INPUT_FORMATS': ( 'DATETIME_FORMAT': ISO_8601,
ISO_8601, 'DATETIME_INPUT_FORMATS': (ISO_8601,),
),
'DATETIME_FORMAT': None,
'TIME_INPUT_FORMATS': ( 'TIME_FORMAT': ISO_8601,
ISO_8601, 'TIME_INPUT_FORMATS': (ISO_8601,),
),
'TIME_FORMAT': None,
# Pending deprecation
'FILTER_BACKEND': None,
# Encoding
'UNICODE_JSON': True,
'COMPACT_JSON': True,
'COERCE_DECIMAL_TO_STRING': True,
'UPLOADED_FILES_USE_URL': True
} }
...@@ -125,11 +123,11 @@ IMPORT_STRINGS = ( ...@@ -125,11 +123,11 @@ IMPORT_STRINGS = (
'DEFAULT_PERMISSION_CLASSES', 'DEFAULT_PERMISSION_CLASSES',
'DEFAULT_THROTTLE_CLASSES', 'DEFAULT_THROTTLE_CLASSES',
'DEFAULT_CONTENT_NEGOTIATION_CLASS', 'DEFAULT_CONTENT_NEGOTIATION_CLASS',
'DEFAULT_METADATA_CLASS',
'DEFAULT_MODEL_SERIALIZER_CLASS', 'DEFAULT_MODEL_SERIALIZER_CLASS',
'DEFAULT_PAGINATION_SERIALIZER_CLASS', 'DEFAULT_PAGINATION_SERIALIZER_CLASS',
'DEFAULT_FILTER_BACKENDS', 'DEFAULT_FILTER_BACKENDS',
'EXCEPTION_HANDLER', 'EXCEPTION_HANDLER',
'FILTER_BACKEND',
'TEST_REQUEST_RENDERER_CLASSES', 'TEST_REQUEST_RENDERER_CLASSES',
'UNAUTHENTICATED_USER', 'UNAUTHENTICATED_USER',
'UNAUTHENTICATED_TOKEN', 'UNAUTHENTICATED_TOKEN',
...@@ -196,15 +194,9 @@ class APISettings(object): ...@@ -196,15 +194,9 @@ class APISettings(object):
if val and attr in self.import_strings: if val and attr in self.import_strings:
val = perform_import(val, attr) val = perform_import(val, attr)
self.validate_setting(attr, val)
# Cache the result # Cache the result
setattr(self, attr, val) setattr(self, attr, val)
return 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) api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
...@@ -10,6 +10,12 @@ a single block in the template. ...@@ -10,6 +10,12 @@ a single block in the template.
background: transparent; background: transparent;
border-top-color: transparent; border-top-color: transparent;
padding-top: 0; 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 { .navbar-inverse .brand a {
...@@ -29,7 +35,7 @@ a single block in the template. ...@@ -29,7 +35,7 @@ a single block in the template.
z-index: 3; z-index: 3;
} }
.navbar .navbar-inner { .navbar {
background: #2C2C2C; background: #2C2C2C;
color: white; color: white;
border: none; border: none;
...@@ -37,7 +43,7 @@ a single block in the template. ...@@ -37,7 +43,7 @@ a single block in the template.
border-radius: 0px; 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; color: white;
} }
...@@ -45,11 +51,11 @@ a single block in the template. ...@@ -45,11 +51,11 @@ a single block in the template.
background: #2C2C2C; 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; color: #A30000;
} }
.navbar .navbar-inner .dropdown-menu li a:hover { .navbar .dropdown-menu li a:hover {
background: #EEEEEE; background: #EEEEEE;
color: #C20000; color: #C20000;
} }
...@@ -61,10 +67,10 @@ html { ...@@ -61,10 +67,10 @@ html {
background: none; background: none;
} }
body, .navbar .navbar-inner .container-fluid { /*body, .navbar .container-fluid {
max-width: 1150px; max-width: 1150px;
margin: 0 auto; margin: 0 auto;
} }*/
body { body {
background: url("../img/grid.png") repeat-x; background: url("../img/grid.png") repeat-x;
...@@ -109,10 +115,6 @@ html, body { ...@@ -109,10 +115,6 @@ html, body {
margin-bottom: 0; margin-bottom: 0;
} }
.well form .help-block {
color: #999999;
}
.nav-tabs { .nav-tabs {
border: 0; border: 0;
} }
...@@ -167,7 +169,7 @@ footer a:hover { ...@@ -167,7 +169,7 @@ footer a:hover {
.page-header { .page-header {
border-bottom: none; border-bottom: none;
padding-bottom: 0px; padding-bottom: 0px;
margin-bottom: 20px; margin: 0;
} }
/* custom general page styles */ /* custom general page styles */
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -33,7 +33,11 @@ h2, h3 { ...@@ -33,7 +33,11 @@ h2, h3 {
} }
ul.breadcrumb { ul.breadcrumb {
margin: 80px 0 0 0; margin: 70px 0 0 0;
}
.breadcrumb li.active a {
color: #777;
} }
form select, form input, form textarea { form select, form input, form textarea {
...@@ -67,5 +71,4 @@ pre { ...@@ -67,5 +71,4 @@ pre {
.page-header { .page-header {
border-bottom: none; border-bottom: none;
padding-bottom: 0px; padding-bottom: 0px;
margin-bottom: 20px;
} }
...@@ -26,22 +26,21 @@ ...@@ -26,22 +26,21 @@
</head> </head>
{% block body %} {% block body %}
<body class="{% block bodyclass %}{% endblock %} container"> <body class="{% block bodyclass %}{% endblock %}">
<div class="wrapper"> <div class="wrapper">
{% block navbar %} {% block navbar %}
<div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}"> <div class="navbar navbar-static-top {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
<div class="navbar-inner"> <div class="container">
<div class="container-fluid">
<span> <span>
{% block branding %} {% block branding %}
<a class='brand' rel="nofollow" href='http://www.django-rest-framework.org'> <a class='navbar-brand' rel="nofollow" href='http://www.django-rest-framework.org'>
Django REST framework <span class="version">{{ version }}</span> Django REST framework <span class="version">{{ version }}</span>
</a> </a>
{% endblock %} {% endblock %}
</span> </span>
<ul class="nav pull-right"> <ul class="nav navbar-nav pull-right">
{% block userlinks %} {% block userlinks %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% optional_logout request user %} {% optional_logout request user %}
...@@ -52,18 +51,17 @@ ...@@ -52,18 +51,17 @@
</ul> </ul>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}
<div class="container">
{% block breadcrumbs %} {% block breadcrumbs %}
<ul class="breadcrumb"> <ul class="breadcrumb">
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %} {% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
<li> {% if forloop.last %}
<a href="{{ breadcrumb_url }}" {% if forloop.last %}class="active"{% endif %}> <li class="active"><a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a></li>
{{ breadcrumb_name }} {% else %}
</a> <li><a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a></li>
{% if not forloop.last %}<span class="divider">&rsaquo;</span>{% endif %} {% endif %}
</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endblock %} {% endblock %}
...@@ -154,7 +152,7 @@ ...@@ -154,7 +152,7 @@
<div class="tab-pane" id="post-object-form"> <div class="tab-pane" id="post-object-form">
{% with form=post_form %} {% with form=post_form %}
<form action="{{ request.get_full_path }}" <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> <fieldset>
{{ post_form }} {{ post_form }}
<div class="form-actions"> <div class="form-actions">
...@@ -199,7 +197,7 @@ ...@@ -199,7 +197,7 @@
{% if put_form %} {% if put_form %}
<div class="tab-pane" id="put-object-form"> <div class="tab-pane" id="put-object-form">
<form action="{{ request.get_full_path }}" <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> <fieldset>
{{ put_form }} {{ put_form }}
<div class="form-actions"> <div class="form-actions">
...@@ -238,6 +236,7 @@ ...@@ -238,6 +236,7 @@
{% endif %} {% endif %}
</div> </div>
<!-- END Content --> <!-- END Content -->
</div><!-- /.container -->
<footer> <footer>
{% block footer %} {% block footer %}
......
{% 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 %}
<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>
<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>
{% 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>
{% 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>
<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>
{% 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>
<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>
<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>
<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>
<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>
<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>
<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>
{% load rest_framework %}
{% for nested_field in field %}
{% render_field nested_field template_pack=template_pack renderer=renderer %}
{% endfor %}
{% 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>
<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>
<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>
<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>
<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>
<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>
<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>
\ No newline at end of file
<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>
{% 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>
{% 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>
<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>
<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>
<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>
<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>
<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>
<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 ...@@ -8,6 +8,7 @@ from django.utils.html import escape
from django.utils.safestring import SafeData, mark_safe from django.utils.safestring import SafeData, mark_safe
from django.utils.html import smart_urlquote from django.utils.html import smart_urlquote
from rest_framework.compat import urlparse, force_text from rest_framework.compat import urlparse, force_text
from rest_framework.renderers import HTMLFormRenderer
import re import re
register = template.Library() register = template.Library()
...@@ -32,6 +33,13 @@ class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])') ...@@ -32,6 +33,13 @@ class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
# And the template tags themselves... # And the template tags themselves...
@register.simple_tag @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): def optional_login(request):
""" """
Include a login snippet if REST framework's login view is in the URLconf. Include a login snippet if REST framework's login view is in the URLconf.
......
...@@ -2,12 +2,11 @@ ...@@ -2,12 +2,11 @@
Helper classes for parsers. Helper classes for parsers.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils import timezone
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.utils import six, timezone
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from django.utils.functional import Promise from django.utils.functional import Promise
from rest_framework.compat import force_text from rest_framework.compat import force_text
from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
import datetime import datetime
import decimal import decimal
import types import types
...@@ -17,45 +16,47 @@ import json ...@@ -17,45 +16,47 @@ import json
class JSONEncoder(json.JSONEncoder): class JSONEncoder(json.JSONEncoder):
""" """
JSONEncoder subclass that knows how to encode date/time/timedelta, 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 # For Date Time string spec, see ECMA 262
# http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 # http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
if isinstance(o, Promise): if isinstance(obj, Promise):
return force_text(o) return force_text(obj)
elif isinstance(o, datetime.datetime): elif isinstance(obj, datetime.datetime):
r = o.isoformat() representation = obj.isoformat()
if o.microsecond: if obj.microsecond:
r = r[:23] + r[26:] representation = representation[:23] + representation[26:]
if r.endswith('+00:00'): if representation.endswith('+00:00'):
r = r[:-6] + 'Z' representation = representation[:-6] + 'Z'
return r return representation
elif isinstance(o, datetime.date): elif isinstance(obj, datetime.date):
return o.isoformat() return obj.isoformat()
elif isinstance(o, datetime.time): elif isinstance(obj, datetime.time):
if timezone and timezone.is_aware(o): if timezone and timezone.is_aware(obj):
raise ValueError("JSON can't represent timezone-aware times.") raise ValueError("JSON can't represent timezone-aware times.")
r = o.isoformat() representation = obj.isoformat()
if o.microsecond: if obj.microsecond:
r = r[:12] representation = representation[:12]
return r return representation
elif isinstance(o, datetime.timedelta): elif isinstance(obj, datetime.timedelta):
return str(o.total_seconds()) return six.text_type(obj.total_seconds())
elif isinstance(o, decimal.Decimal): elif isinstance(obj, decimal.Decimal):
return str(o) # Serializers will coerce decimals to strings by default.
elif isinstance(o, QuerySet): return float(obj)
return list(o) elif isinstance(obj, QuerySet):
elif hasattr(o, 'tolist'): return tuple(obj)
return o.tolist() elif hasattr(obj, 'tolist'):
elif hasattr(o, '__getitem__'): # Numpy arrays and array scalars.
return obj.tolist()
elif hasattr(obj, '__getitem__'):
try: try:
return dict(o) return dict(obj)
except: except:
pass pass
elif hasattr(o, '__iter__'): elif hasattr(obj, '__iter__'):
return [i for i in o] return tuple(item for item in obj)
return super(JSONEncoder, self).default(o) return super(JSONEncoder, self).default(obj)
try: try:
...@@ -71,7 +72,7 @@ else: ...@@ -71,7 +72,7 @@ else:
than the usual behaviour of sorting the keys. than the usual behaviour of sorting the keys.
""" """
def represent_decimal(self, data): 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): def represent_mapping(self, tag, mapping, flow_style=None):
value = [] value = []
...@@ -106,14 +107,14 @@ else: ...@@ -106,14 +107,14 @@ else:
SortedDict, SortedDict,
yaml.representer.SafeRepresenter.represent_dict yaml.representer.SafeRepresenter.represent_dict
) )
SafeDumper.add_representer( # SafeDumper.add_representer(
DictWithMetadata, # DictWithMetadata,
yaml.representer.SafeRepresenter.represent_dict # yaml.representer.SafeRepresenter.represent_dict
) # )
SafeDumper.add_representer( # SafeDumper.add_representer(
SortedDictWithMetadata, # SortedDictWithMetadata,
yaml.representer.SafeRepresenter.represent_dict # yaml.representer.SafeRepresenter.represent_dict
) # )
SafeDumper.add_representer( SafeDumper.add_representer(
types.GeneratorType, types.GeneratorType,
yaml.representer.SafeRepresenter.represent_list yaml.representer.SafeRepresenter.represent_list
......
"""
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)
}
"""
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
"""
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
"""
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)
"""
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)
"""
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 ...@@ -5,7 +5,6 @@ from __future__ import unicode_literals
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404 from django.http import Http404
from django.utils.datastructures import SortedDict
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from rest_framework import status, exceptions from rest_framework import status, exceptions
from rest_framework.compat import smart_text, HttpResponseBase, View from rest_framework.compat import smart_text, HttpResponseBase, View
...@@ -51,7 +50,8 @@ def exception_handler(exc): ...@@ -51,7 +50,8 @@ def exception_handler(exc):
Returns the response that should be used for any given exception. Returns the response that should be used for any given exception.
By default we handle the REST framework `APIException`, and also 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 Any unhandled exceptions may return `None`, which will cause a 500 error
to be raised. to be raised.
...@@ -61,20 +61,22 @@ def exception_handler(exc): ...@@ -61,20 +61,22 @@ def exception_handler(exc):
if getattr(exc, 'auth_header', None): if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None): if getattr(exc, 'wait', None):
headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
headers['Retry-After'] = '%d' % exc.wait headers['Retry-After'] = '%d' % exc.wait
return Response({'detail': exc.detail}, if isinstance(exc.detail, (list, dict)):
status=exc.status_code, data = exc.detail
headers=headers) else:
data = {'detail': exc.detail}
return Response(data, status=exc.status_code, headers=headers)
elif isinstance(exc, Http404): elif isinstance(exc, Http404):
return Response({'detail': 'Not found'}, data = {'detail': 'Not found'}
status=status.HTTP_404_NOT_FOUND) return Response(data, status=status.HTTP_404_NOT_FOUND)
elif isinstance(exc, PermissionDenied): elif isinstance(exc, PermissionDenied):
return Response({'detail': 'Permission denied'}, data = {'detail': 'Permission denied'}
status=status.HTTP_403_FORBIDDEN) return Response(data, status=status.HTTP_403_FORBIDDEN)
# Note: Unhandled exceptions will raise a 500 error. # Note: Unhandled exceptions will raise a 500 error.
return None return None
...@@ -89,8 +91,9 @@ class APIView(View): ...@@ -89,8 +91,9 @@ class APIView(View):
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS 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 settings = api_settings
@classmethod @classmethod
...@@ -408,22 +411,8 @@ class APIView(View): ...@@ -408,22 +411,8 @@ class APIView(View):
def options(self, request, *args, **kwargs): def options(self, request, *args, **kwargs):
""" """
Handler method for HTTP 'OPTIONS' request. 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 if self.metadata_class is None:
# generic views override this implementation and add additional return self.http_method_not_allowed(request, *args, **kwargs)
# information for POST and PUT methods, based on the serializer. data = self.metadata_class().determine_metadata(request, self)
ret = SortedDict() return Response(data, status=status.HTTP_200_OK)
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
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
def foobar(): def foobar():
...@@ -178,9 +177,3 @@ class NullableOneToOneSource(RESTFrameworkModel): ...@@ -178,9 +177,3 @@ class NullableOneToOneSource(RESTFrameworkModel):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
target = models.OneToOneField(OneToOneTarget, null=True, blank=True, target = models.OneToOneField(OneToOneTarget, null=True, blank=True,
related_name='nullable_source') related_name='nullable_source')
# Serializer used to test BasicModel
class BasicModelSerializer(serializers.ModelSerializer):
class Meta:
model = BasicModel
# 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)
from rest_framework import serializers
from tests.models import NullableForeignKeySource
class NullableFKSourceSerializer(serializers.ModelSerializer):
class Meta:
model = NullableForeignKeySource
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'
from __future__ import unicode_literals # from __future__ import unicode_literals
from django.test import TestCase # from django.test import TestCase
from django.utils import six # from django.utils import six
from rest_framework import serializers # from rest_framework import serializers
from rest_framework.compat import BytesIO # from rest_framework.compat import BytesIO
import datetime # import datetime
class UploadedFile(object): # class UploadedFile(object):
def __init__(self, file=None, created=None): # def __init__(self, file=None, created=None):
self.file = file # self.file = file
self.created = created or datetime.datetime.now() # self.created = created or datetime.datetime.now()
class UploadedFileSerializer(serializers.Serializer): # class UploadedFileSerializer(serializers.Serializer):
file = serializers.FileField(required=False) # file = serializers.FileField(required=False)
created = serializers.DateTimeField() # created = serializers.DateTimeField()
def restore_object(self, attrs, instance=None): # def restore_object(self, attrs, instance=None):
if instance: # if instance:
instance.file = attrs['file'] # instance.file = attrs['file']
instance.created = attrs['created'] # instance.created = attrs['created']
return instance # return instance
return UploadedFile(**attrs) # return UploadedFile(**attrs)
class FileSerializerTests(TestCase): # class FileSerializerTests(TestCase):
def test_create(self): # def test_create(self):
now = datetime.datetime.now() # now = datetime.datetime.now()
file = BytesIO(six.b('stuff')) # file = BytesIO(six.b('stuff'))
file.name = 'stuff.txt' # file.name = 'stuff.txt'
file.size = len(file.getvalue()) # file.size = len(file.getvalue())
serializer = UploadedFileSerializer(data={'created': now}, files={'file': file}) # serializer = UploadedFileSerializer(data={'created': now}, files={'file': file})
uploaded_file = UploadedFile(file=file, created=now) # uploaded_file = UploadedFile(file=file, created=now)
self.assertTrue(serializer.is_valid()) # self.assertTrue(serializer.is_valid())
self.assertEqual(serializer.object.created, uploaded_file.created) # self.assertEqual(serializer.object.created, uploaded_file.created)
self.assertEqual(serializer.object.file, uploaded_file.file) # self.assertEqual(serializer.object.file, uploaded_file.file)
self.assertFalse(serializer.object is uploaded_file) # self.assertFalse(serializer.object is uploaded_file)
def test_creation_failure(self): # def test_creation_failure(self):
""" # """
Passing files=None should result in an ValidationError # Passing files=None should result in an ValidationError
Regression test for: # Regression test for:
https://github.com/tomchristie/django-rest-framework/issues/542 # https://github.com/tomchristie/django-rest-framework/issues/542
""" # """
now = datetime.datetime.now() # now = datetime.datetime.now()
serializer = UploadedFileSerializer(data={'created': now}) # serializer = UploadedFileSerializer(data={'created': now})
self.assertTrue(serializer.is_valid()) # self.assertTrue(serializer.is_valid())
self.assertEqual(serializer.object.created, now) # self.assertEqual(serializer.object.created, now)
self.assertIsNone(serializer.object.file) # self.assertIsNone(serializer.object.file)
def test_remove_with_empty_string(self): # def test_remove_with_empty_string(self):
""" # """
Passing empty string as data should cause file to be removed # Passing empty string as data should cause file to be removed
Test for: # Test for:
https://github.com/tomchristie/django-rest-framework/issues/937 # https://github.com/tomchristie/django-rest-framework/issues/937
""" # """
now = datetime.datetime.now() # now = datetime.datetime.now()
file = BytesIO(six.b('stuff')) # file = BytesIO(six.b('stuff'))
file.name = 'stuff.txt' # file.name = 'stuff.txt'
file.size = len(file.getvalue()) # 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': ''}) # serializer = UploadedFileSerializer(instance=uploaded_file, data={'created': now, 'file': ''})
self.assertTrue(serializer.is_valid()) # self.assertTrue(serializer.is_valid())
self.assertEqual(serializer.object.created, uploaded_file.created) # self.assertEqual(serializer.object.created, uploaded_file.created)
self.assertIsNone(serializer.object.file) # self.assertIsNone(serializer.object.file)
def test_validation_error_with_non_file(self): # def test_validation_error_with_non_file(self):
""" # """
Passing non-files should raise a validation error. # Passing non-files should raise a validation error.
""" # """
now = datetime.datetime.now() # now = datetime.datetime.now()
errmsg = 'No file was submitted. Check the encoding type on the form.' # errmsg = 'No file was submitted. Check the encoding type on the form.'
serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'}) # serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'})
self.assertFalse(serializer.is_valid()) # self.assertFalse(serializer.is_valid())
self.assertEqual(serializer.errors, {'file': [errmsg]}) # self.assertEqual(serializer.errors, {'file': [errmsg]})
def test_validation_with_no_data(self): # def test_validation_with_no_data(self):
""" # """
Validation should still function when no data dictionary is provided. # Validation should still function when no data dictionary is provided.
""" # """
uploaded_file = BytesIO(six.b('stuff')) # uploaded_file = BytesIO(six.b('stuff'))
uploaded_file.name = 'stuff.txt' # uploaded_file.name = 'stuff.txt'
uploaded_file.size = len(uploaded_file.getvalue()) # uploaded_file.size = len(uploaded_file.getvalue())
serializer = UploadedFileSerializer(files={'file': uploaded_file}) # serializer = UploadedFileSerializer(files={'file': uploaded_file})
self.assertFalse(serializer.is_valid()) # self.assertFalse(serializer.is_valid())
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']
from django.core.urlresolvers import reverse # from django.core.urlresolvers import reverse
from django.conf.urls import patterns, url # from django.conf.urls import patterns, url
from rest_framework.test import APITestCase # from rest_framework import serializers, generics
from tests.models import NullableForeignKeySource # from rest_framework.test import APITestCase
from tests.serializers import NullableFKSourceSerializer # from tests.models import NullableForeignKeySource
from tests.views import NullableFKSourceDetail
urlpatterns = patterns( # class NullableFKSourceSerializer(serializers.ModelSerializer):
'', # class Meta:
url(r'^objects/(?P<pk>\d+)/$', NullableFKSourceDetail.as_view(), name='object-detail'), # model = NullableForeignKeySource
)
class NullableForeignKeyTests(APITestCase): # class NullableFKSourceDetail(generics.RetrieveUpdateDestroyAPIView):
""" # queryset = NullableForeignKeySource.objects.all()
DRF should be able to handle nullable foreign keys when a test # serializer_class = NullableFKSourceSerializer
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) # 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 ...@@ -4,7 +4,7 @@ from decimal import Decimal
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.test import TestCase from django.test import TestCase
from django.utils import unittest 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.compat import django_filters
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from .models import BasicModel, FilterableItem from .models import BasicModel, FilterableItem
...@@ -22,11 +22,22 @@ def split_arguments_from_url(url): ...@@ -22,11 +22,22 @@ def split_arguments_from_url(url):
return path, args return path, args
class BasicSerializer(serializers.ModelSerializer):
class Meta:
model = BasicModel
class FilterableItemSerializer(serializers.ModelSerializer):
class Meta:
model = FilterableItem
class RootView(generics.ListCreateAPIView): class RootView(generics.ListCreateAPIView):
""" """
Example description for OPTIONS. Example description for OPTIONS.
""" """
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
paginate_by = 10 paginate_by = 10
...@@ -34,14 +45,16 @@ class DefaultPageSizeKwargView(generics.ListAPIView): ...@@ -34,14 +45,16 @@ class DefaultPageSizeKwargView(generics.ListAPIView):
""" """
View for testing default paginate_by_param usage View for testing default paginate_by_param usage
""" """
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
class PaginateByParamView(generics.ListAPIView): class PaginateByParamView(generics.ListAPIView):
""" """
View for testing custom paginate_by_param usage View for testing custom paginate_by_param usage
""" """
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
paginate_by_param = 'page_size' paginate_by_param = 'page_size'
...@@ -49,7 +62,8 @@ class MaxPaginateByView(generics.ListAPIView): ...@@ -49,7 +62,8 @@ class MaxPaginateByView(generics.ListAPIView):
""" """
View for testing custom max_paginate_by usage View for testing custom max_paginate_by usage
""" """
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
paginate_by = 3 paginate_by = 3
max_paginate_by = 5 max_paginate_by = 5
paginate_by_param = 'page_size' paginate_by_param = 'page_size'
...@@ -121,7 +135,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): ...@@ -121,7 +135,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
self.objects = FilterableItem.objects self.objects = FilterableItem.objects
self.data = [ 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() for obj in self.objects.all()
] ]
...@@ -140,7 +154,8 @@ class IntegrationTestPaginationAndFiltering(TestCase): ...@@ -140,7 +154,8 @@ class IntegrationTestPaginationAndFiltering(TestCase):
fields = ['text', 'decimal', 'date'] fields = ['text', 'decimal', 'date']
class FilterFieldsRootView(generics.ListCreateAPIView): class FilterFieldsRootView(generics.ListCreateAPIView):
model = FilterableItem queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
paginate_by = 10 paginate_by = 10
filter_class = DecimalFilter filter_class = DecimalFilter
filter_backends = (filters.DjangoFilterBackend,) filter_backends = (filters.DjangoFilterBackend,)
...@@ -188,7 +203,8 @@ class IntegrationTestPaginationAndFiltering(TestCase): ...@@ -188,7 +203,8 @@ class IntegrationTestPaginationAndFiltering(TestCase):
return queryset.filter(decimal__lt=Decimal(request.GET['decimal'])) return queryset.filter(decimal__lt=Decimal(request.GET['decimal']))
class BasicFilterFieldsRootView(generics.ListCreateAPIView): class BasicFilterFieldsRootView(generics.ListCreateAPIView):
model = FilterableItem queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
paginate_by = 10 paginate_by = 10
filter_backends = (DecimalFilterBackend,) filter_backends = (DecimalFilterBackend,)
...@@ -365,7 +381,7 @@ class TestMaxPaginateByParam(TestCase): ...@@ -365,7 +381,7 @@ class TestMaxPaginateByParam(TestCase):
# Tests for context in pagination serializers # Tests for context in pagination serializers
class CustomField(serializers.Field): class CustomField(serializers.ReadOnlyField):
def to_native(self, value): def to_native(self, value):
if 'view' not in self.context: if 'view' not in self.context:
raise RuntimeError("context isn't getting passed into custom field") raise RuntimeError("context isn't getting passed into custom field")
...@@ -375,10 +391,10 @@ class CustomField(serializers.Field): ...@@ -375,10 +391,10 @@ class CustomField(serializers.Field):
class BasicModelSerializer(serializers.Serializer): class BasicModelSerializer(serializers.Serializer):
text = CustomField() text = CustomField()
def __init__(self, *args, **kwargs): def to_native(self, value):
super(BasicModelSerializer, self).__init__(*args, **kwargs)
if 'view' not in self.context: 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): class TestContextPassedToCustomField(TestCase):
...@@ -387,7 +403,7 @@ class TestContextPassedToCustomField(TestCase): ...@@ -387,7 +403,7 @@ class TestContextPassedToCustomField(TestCase):
def test_with_pagination(self): def test_with_pagination(self):
class ListView(generics.ListCreateAPIView): class ListView(generics.ListCreateAPIView):
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicModelSerializer serializer_class = BasicModelSerializer
paginate_by = 1 paginate_by = 1
...@@ -407,7 +423,7 @@ class LinksSerializer(serializers.Serializer): ...@@ -407,7 +423,7 @@ class LinksSerializer(serializers.Serializer):
class CustomPaginationSerializer(pagination.BasePaginationSerializer): class CustomPaginationSerializer(pagination.BasePaginationSerializer):
links = LinksSerializer(source='*') # Takes the page object as the source 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' results_field = 'objects'
......
...@@ -3,7 +3,7 @@ from django.contrib.auth.models import User, Permission, Group ...@@ -3,7 +3,7 @@ from django.contrib.auth.models import User, Permission, Group
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import unittest 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.compat import guardian, get_model_name
from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.filters import DjangoObjectPermissionsFilter
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
...@@ -13,14 +13,21 @@ import base64 ...@@ -13,14 +13,21 @@ import base64
factory = APIRequestFactory() factory = APIRequestFactory()
class RootView(generics.ListCreateAPIView): class BasicSerializer(serializers.ModelSerializer):
class Meta:
model = BasicModel model = BasicModel
class RootView(generics.ListCreateAPIView):
queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
permission_classes = [permissions.DjangoModelPermissions] permission_classes = [permissions.DjangoModelPermissions]
class InstanceView(generics.RetrieveUpdateDestroyAPIView): class InstanceView(generics.RetrieveUpdateDestroyAPIView):
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
permission_classes = [permissions.DjangoModelPermissions] permission_classes = [permissions.DjangoModelPermissions]
...@@ -88,72 +95,59 @@ class ModelPermissionsIntegrationTests(TestCase): ...@@ -88,72 +95,59 @@ class ModelPermissionsIntegrationTests(TestCase):
response = instance_view(request, pk=1) response = instance_view(request, pk=1)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_has_put_as_create_permissions(self): # def test_options_permitted(self):
# User only has update permissions - should be able to update an entity. # request = factory.options(
request = factory.put('/1', {'text': 'foobar'}, format='json', # '/',
HTTP_AUTHORIZATION=self.updateonly_credentials) # HTTP_AUTHORIZATION=self.permitted_credentials
response = instance_view(request, pk='1') # )
self.assertEqual(response.status_code, status.HTTP_200_OK) # response = root_view(request, pk='1')
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# But if PUTing to a new entity, permission should be denied. # self.assertIn('actions', response.data)
request = factory.put('/2', {'text': 'foobar'}, format='json', # self.assertEqual(list(response.data['actions'].keys()), ['POST'])
HTTP_AUTHORIZATION=self.updateonly_credentials)
response = instance_view(request, pk='2') # request = factory.options(
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) # '/1',
# HTTP_AUTHORIZATION=self.permitted_credentials
def test_options_permitted(self): # )
request = factory.options( # response = instance_view(request, pk='1')
'/', # self.assertEqual(response.status_code, status.HTTP_200_OK)
HTTP_AUTHORIZATION=self.permitted_credentials # self.assertIn('actions', response.data)
) # self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) # def test_options_disallowed(self):
self.assertIn('actions', response.data) # request = factory.options(
self.assertEqual(list(response.data['actions'].keys()), ['POST']) # '/',
# HTTP_AUTHORIZATION=self.disallowed_credentials
request = factory.options( # )
'/1', # response = root_view(request, pk='1')
HTTP_AUTHORIZATION=self.permitted_credentials # self.assertEqual(response.status_code, status.HTTP_200_OK)
) # self.assertNotIn('actions', response.data)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) # request = factory.options(
self.assertIn('actions', response.data) # '/1',
self.assertEqual(list(response.data['actions'].keys()), ['PUT']) # HTTP_AUTHORIZATION=self.disallowed_credentials
# )
def test_options_disallowed(self): # response = instance_view(request, pk='1')
request = factory.options( # self.assertEqual(response.status_code, status.HTTP_200_OK)
'/', # self.assertNotIn('actions', response.data)
HTTP_AUTHORIZATION=self.disallowed_credentials
) # def test_options_updateonly(self):
response = root_view(request, pk='1') # request = factory.options(
self.assertEqual(response.status_code, status.HTTP_200_OK) # '/',
self.assertNotIn('actions', response.data) # HTTP_AUTHORIZATION=self.updateonly_credentials
# )
request = factory.options( # response = root_view(request, pk='1')
'/1', # self.assertEqual(response.status_code, status.HTTP_200_OK)
HTTP_AUTHORIZATION=self.disallowed_credentials # self.assertNotIn('actions', response.data)
)
response = instance_view(request, pk='1') # request = factory.options(
self.assertEqual(response.status_code, status.HTTP_200_OK) # '/1',
self.assertNotIn('actions', response.data) # HTTP_AUTHORIZATION=self.updateonly_credentials
# )
def test_options_updateonly(self): # response = instance_view(request, pk='1')
request = factory.options( # self.assertEqual(response.status_code, status.HTTP_200_OK)
'/', # self.assertIn('actions', response.data)
HTTP_AUTHORIZATION=self.updateonly_credentials # self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
)
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'])
class BasicPermModel(models.Model): class BasicPermModel(models.Model):
...@@ -167,6 +161,11 @@ 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 # Custom object-level permission, that includes 'view' permissions
class ViewObjectPermissions(permissions.DjangoObjectPermissions): class ViewObjectPermissions(permissions.DjangoObjectPermissions):
perms_map = { perms_map = {
...@@ -181,7 +180,8 @@ class ViewObjectPermissions(permissions.DjangoObjectPermissions): ...@@ -181,7 +180,8 @@ class ViewObjectPermissions(permissions.DjangoObjectPermissions):
class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView): class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
model = BasicPermModel queryset = BasicPermModel.objects.all()
serializer_class = BasicPermSerializer
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
permission_classes = [ViewObjectPermissions] permission_classes = [ViewObjectPermissions]
...@@ -189,7 +189,8 @@ object_permissions_view = ObjectPermissionInstanceView.as_view() ...@@ -189,7 +189,8 @@ object_permissions_view = ObjectPermissionInstanceView.as_view()
class ObjectPermissionListView(generics.ListAPIView): class ObjectPermissionListView(generics.ListAPIView):
model = BasicPermModel queryset = BasicPermModel.objects.all()
serializer_class = BasicPermSerializer
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
permission_classes = [ViewObjectPermissions] permission_classes = [ViewObjectPermissions]
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment