Commit 0cc99079 by José Padilla

Merge branch 'version-3.1' into oauth_as_package

Conflicts:
	requirements-test.txt
	rest_framework/compat.py
	tests/settings.py
	tox.ini
parents 1aa77830 3a5b3772
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
*~ *~
.* .*
html/ site/
htmlcov/ htmlcov/
coverage/ coverage/
build/ build/
......
language: python language: python
python: 2.7 sudo: false
env: env:
- TOX_ENV=flake8 - TOX_ENV=py27-flake8
- TOX_ENV=py3.4-django1.7 - TOX_ENV=py27-docs
- TOX_ENV=py3.3-django1.7 - TOX_ENV=py34-django17
- TOX_ENV=py3.2-django1.7 - TOX_ENV=py33-django17
- TOX_ENV=py2.7-django1.7 - TOX_ENV=py32-django17
- TOX_ENV=py3.4-django1.6 - TOX_ENV=py27-django17
- TOX_ENV=py3.3-django1.6 - TOX_ENV=py34-django16
- TOX_ENV=py3.2-django1.6 - TOX_ENV=py33-django16
- TOX_ENV=py2.7-django1.6 - TOX_ENV=py32-django16
- TOX_ENV=py2.6-django1.6 - TOX_ENV=py27-django16
- TOX_ENV=py3.4-django1.5 - TOX_ENV=py26-django16
- TOX_ENV=py3.3-django1.5 - TOX_ENV=py34-django15
- TOX_ENV=py3.2-django1.5 - TOX_ENV=py33-django15
- TOX_ENV=py2.7-django1.5 - TOX_ENV=py32-django15
- TOX_ENV=py2.6-django1.5 - TOX_ENV=py27-django15
- TOX_ENV=py2.7-django1.4 - TOX_ENV=py26-django15
- TOX_ENV=py2.6-django1.4 - TOX_ENV=py27-django14
- TOX_ENV=py26-django14
- TOX_ENV=py34-djangomaster
- TOX_ENV=py33-djangomaster
- TOX_ENV=py32-djangomaster
- TOX_ENV=py27-djangomaster
matrix:
fast_finish: true
allow_failures:
- env: TOX_ENV=py34-djangomaster
- env: TOX_ENV=py33-djangomaster
- env: TOX_ENV=py32-djangomaster
- env: TOX_ENV=py27-djangomaster
install: install:
- "pip install tox --download-cache $HOME/.pip-cache" - pip install tox
script: script:
- tox -e $TOX_ENV - tox -e $TOX_ENV
...@@ -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.
...@@ -101,15 +101,15 @@ There are many great markdown editors that make working with the documentation r ...@@ -101,15 +101,15 @@ There are many great markdown editors that make working with the documentation r
## Building the documentation ## Building the documentation
To build the documentation, simply run the `mkdocs.py` script. To build the documentation, install MkDocs with `pip install mkdocs` and then run the following command.
./mkdocs.py mkdocs build
This will build the html output into the `html` directory. This will build the html output into the `html` directory.
You can build the documentation and open a preview in a browser window by using the `-p` flag. You can build the documentation and open a preview in a browser window by using the `serve` command.
./mkdocs.py -p mkdocs serve
## Language style ## Language style
......
recursive-include rest_framework/static *.js *.css *.png recursive-include rest_framework/static *.js *.css *.png *.eot *.svg *.ttf *.woff
recursive-include rest_framework/templates *.html recursive-include rest_framework/templates *.html
recursive-exclude * __pycache__ recursive-exclude * __pycache__
recursive-exclude * *.py[co] recursive-exclude * *.py[co]
# Django REST framework # Django REST framework
[![build-status-image]][travis] [![build-status-image]][travis]
[![pypi-version]][pypi]
**Awesome web-browseable Web APIs.** **Awesome web-browseable Web APIs.**
**Note**: Full documentation for the project is available at [http://www.django-rest-framework.org][docs]. Full documentation for the project is available at [http://www.django-rest-framework.org][docs].
---
**Note**: The incoming 3.0 version has now been merged to the `master` branch on GitHub. For the source of the currently available PyPI version, please see the `2.4.4` tag.
---
# Overview # Overview
...@@ -26,8 +33,8 @@ There is a live example API for testing purposes, [available here][sandbox]. ...@@ -26,8 +33,8 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements # Requirements
* Python (2.6.5+, 2.7, 3.2, 3.3) * 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
...@@ -175,6 +182,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -175,6 +182,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master [build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master [travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
[pypi-version]: https://pypip.in/version/djangorestframework/badge.svg
[pypi]: https://pypi.python.org/pypi/djangorestframework
[twitter]: https://twitter.com/_tomchristie [twitter]: https://twitter.com/_tomchristie
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
......
www.django-rest-framework.org
<a class="github" href="authentication.py"></a> source: authentication.py
# Authentication # Authentication
...@@ -168,12 +168,13 @@ The `curl` command line tool may be useful for testing token authenticated APIs. ...@@ -168,12 +168,13 @@ The `curl` command line tool may be useful for testing token authenticated APIs.
If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal. If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
@receiver(post_save, sender=get_user_model()) @receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs): def create_auth_token(sender, instance=None, created=False, **kwargs):
if created: if created:
Token.objects.create(user=instance) Token.objects.create(user=instance)
...@@ -190,9 +191,10 @@ If you've already created some users, you can generate tokens for all existing u ...@@ -190,9 +191,10 @@ If you've already created some users, you can generate tokens for all existing u
When using `TokenAuthentication`, you may want to provide a mechanism for clients to obtain a token given the username and password. REST framework provides a built-in view to provide this behavior. To use it, add the `obtain_auth_token` view to your URLconf: When using `TokenAuthentication`, you may want to provide a mechanism for clients to obtain a token given the username and password. REST framework provides a built-in view to provide this behavior. To use it, add the `obtain_auth_token` view to your URLconf:
urlpatterns += patterns('', from rest_framework.authtoken import views
url(r'^api-token-auth/', 'rest_framework.authtoken.views.obtain_auth_token') urlpatterns += [
) url(r'^api-token-auth/', views.obtain_auth_token)
]
Note that the URL part of the pattern can be whatever you want to use. Note that the URL part of the pattern can be whatever you want to use.
...@@ -297,7 +299,7 @@ HTTP digest authentication is a widely implemented scheme that was intended to r ...@@ -297,7 +299,7 @@ HTTP digest authentication is a widely implemented scheme that was intended to r
## Django OAuth Toolkit ## Django OAuth Toolkit
The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excelllent [OAuthLib][oauthlib]. The package is well documented, and comes as a recommended alternative for OAuth 2.0 support. The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and comes as a recommended alternative for OAuth 2.0 support.
## Django OAuth2 Consumer ## Django OAuth2 Consumer
...@@ -315,6 +317,10 @@ The [HawkREST][hawkrest] library builds on the [Mohawk][mohawk] library to let y ...@@ -315,6 +317,10 @@ The [HawkREST][hawkrest] library builds on the [Mohawk][mohawk] library to let y
HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a way to achieve origin authentication and message integrity for HTTP messages. Similar to [Amazon's HTTP Signature scheme][amazon-http-signature], used by many of its services, it permits stateless, per-request authentication. [Elvio Toccalino][etoccalino] maintains the [djangorestframework-httpsignature][djangorestframework-httpsignature] package which provides an easy to use HTTP Signature Authentication mechanism. HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a way to achieve origin authentication and message integrity for HTTP messages. Similar to [Amazon's HTTP Signature scheme][amazon-http-signature], used by many of its services, it permits stateless, per-request authentication. [Elvio Toccalino][etoccalino] maintains the [djangorestframework-httpsignature][djangorestframework-httpsignature] package which provides an easy to use HTTP Signature Authentication mechanism.
## Djoser
[Djoser][djoser] library provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. The package works with a custom user model and it uses token based authentication. This is a ready to use REST implementation of Django authentication system.
[cite]: http://jacobian.org/writing/rest-worst-practices/ [cite]: http://jacobian.org/writing/rest-worst-practices/
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 [http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
[http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4 [http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
...@@ -349,3 +355,4 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a ...@@ -349,3 +355,4 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
[hawk]: https://github.com/hueniverse/hawk [hawk]: https://github.com/hueniverse/hawk
[mohawk]: http://mohawk.readthedocs.org/en/latest/ [mohawk]: http://mohawk.readthedocs.org/en/latest/
[mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05 [mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
[djoser]: https://github.com/sunscrapers/djoser
<a class="github" href="negotiation.py"></a> source: negotiation.py
# Content negotiation # Content negotiation
......
<a class="github" href="exceptions.py"></a> source: exceptions.py
# Exceptions # Exceptions
...@@ -100,7 +100,7 @@ For example, if your API relies on a third party service that may sometimes be u ...@@ -100,7 +100,7 @@ For example, if your API relies on a third party service that may sometimes be u
**Signature:** `ParseError(detail=None)` **Signature:** `ParseError(detail=None)`
Raised if the request contains malformed data when accessing `request.DATA` or `request.FILES`. Raised if the request contains malformed data when accessing `request.data`.
By default this exception results in a response with the HTTP status code "400 Bad Request". By default this exception results in a response with the HTTP status code "400 Bad Request".
...@@ -140,7 +140,7 @@ By default this exception results in a response with the HTTP status code "405 M ...@@ -140,7 +140,7 @@ By default this exception results in a response with the HTTP status code "405 M
**Signature:** `UnsupportedMediaType(media_type, detail=None)` **Signature:** `UnsupportedMediaType(media_type, detail=None)`
Raised if there are no parsers that can handle the content type of the request data when accessing `request.DATA` or `request.FILES`. Raised if there are no parsers that can handle the content type of the request data when accessing `request.data`.
By default this exception results in a response with the HTTP status code "415 Unsupported Media Type". By default this exception results in a response with the HTTP status code "415 Unsupported Media Type".
...@@ -152,5 +152,23 @@ Raised when an incoming request fails the throttling checks. ...@@ -152,5 +152,23 @@ Raised when an incoming request fails the throttling checks.
By default this exception results in a response with the HTTP status code "429 Too Many Requests". By default this exception results in a response with the HTTP status code "429 Too Many Requests".
## ValidationError
**Signature:** `ValidationError(detail)`
The `ValidationError` exception is slightly different from the other `APIException` classes:
* The `detail` argument is mandatory, not optional.
* The `detail` argument may be a list or dictionary of error details, and may also be a nested data structure.
* By convention you should import the serializers module and use a fully qualified `ValidationError` style, in order to differentiate it from Django's built-in validation error. For example. `raise serializers.ValidationError('This field must be an integer value.')`
The `ValidationError` class should be used for serializer and field validation, and by validator classes. It is also raised when calling `serializer.is_valid` with the `raise_exception` keyword argument:
serializer.is_valid(raise_exception=True)
The generic views use the `raise_exception=True` flag, which means that you can override the style of validation error responses globally in your API. To do so, use a custom exception handler, as described above.
By default this exception results in a response with the HTTP status code "400 Bad Request".
[cite]: http://www.doughellmann.com/articles/how-tos/python-exception-handling/index.html [cite]: http://www.doughellmann.com/articles/how-tos/python-exception-handling/index.html
[authentication]: authentication.md [authentication]: authentication.md
<a class="github" href="filters.py"></a> source: filters.py
# Filtering # Filtering
......
<a class="github" href="urlpatterns.py"></a> source: urlpatterns.py
# Format suffixes # Format suffixes
...@@ -26,12 +26,13 @@ Arguments: ...@@ -26,12 +26,13 @@ Arguments:
Example: Example:
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
from blog import views
urlpatterns = patterns('blog.views', urlpatterns = [
url(r'^/$', 'api_root'), url(r'^/$', views.apt_root),
url(r'^comments/$', 'comment_list'), url(r'^comments/$', views.comment_list),
url(r'^comments/(?P<pk>[0-9]+)/$', 'comment_detail') url(r'^comments/(?P<pk>[0-9]+)/$', views.comment_detail)
) ]
urlpatterns = format_suffix_patterns(urlpatterns, allowed=['json', 'html']) urlpatterns = format_suffix_patterns(urlpatterns, allowed=['json', 'html'])
......
<a class="github" href="mixins.py"></a> source: mixins.py
<a class="github" href="generics.py"></a> generics.py
---
**Note**: This is the documentation for the **version 3.0** of REST framework. Documentation for [version 2.4](http://tomchristie.github.io/rest-framework-2-docs/) is also available.
---
# Generic views # Generic views
...@@ -7,7 +13,7 @@ ...@@ -7,7 +13,7 @@
> >
> &mdash; [Django Documentation][cite] > &mdash; [Django Documentation][cite]
One of the key benefits of class based views is the way they allow you to compose bits of reusable behaviour. REST framework takes advantage of this by providing a number of pre-built views that provide for commonly used patterns. One of the key benefits of class based views is the way they allow you to compose bits of reusable behavior. REST framework takes advantage of this by providing a number of pre-built views that provide for commonly used patterns.
The generic views provided by REST framework allow you to quickly build API views that map closely to your database models. The generic views provided by REST framework allow you to quickly build API views that map closely to your database models.
...@@ -171,24 +177,26 @@ For example: ...@@ -171,24 +177,26 @@ For example:
return 20 return 20
return 100 return 100
**Save / deletion hooks**: **Save and deletion hooks**:
The following methods are provided as placeholder interfaces. They contain empty implementations and are not called directly by `GenericAPIView`, but they are overridden and used by some of the mixin classes. The following methods are provided by the mixin classes, and provide easy overriding of the object save or deletion behavior.
* `pre_save(self, obj)` - A hook that is called before saving an object. * `perform_create(self, serializer)` - Called by `CreateModelMixin` when saving a new object instance.
* `post_save(self, obj, created=False)` - A hook that is called after saving an object. * `perform_update(self, serializer)` - Called by `UpdateModelMixin` when saving an existing object instance.
* `pre_delete(self, obj)` - A hook that is called before deleting an object. * `perform_destroy(self, instance)` - Called by `DestroyModelMixin` when deleting an object instance.
* `post_delete(self, obj)` - A hook that is called after deleting an object.
The `pre_save` method in particular is a useful hook for setting attributes that are implicit in the request, but are not part of the request data. For instance, you might set an attribute on the object based on the request user, or based on a URL keyword argument. These hooks are particularly useful for setting attributes that are implicit in the request, but are not part of the request data. For instance, you might set an attribute on the object based on the request user, or based on a URL keyword argument.
def pre_save(self, obj): def perform_create(self, serializer):
""" serializer.save(user=self.request.user)
Set the object's owner, based on the incoming request.
""" These override points are also particularly useful for adding behavior that occurs before or after saving an object, such as emailing a confirmation, or logging the update.
obj.owner = self.request.user
def perform_update(self, serializer):
instance = serializer.save()
send_email_confirmation(user=self.request.user, modified=instance)
Remember that the `pre_save()` method is not called by `GenericAPIView` itself, but it is called by `create()` and `update()` methods on the `CreateModelMixin` and `UpdateModelMixin` classes. **Note**: These methods replace the old-style version 2.x `pre_save`, `post_save`, `pre_delete` and `post_delete` methods, which are no longer available.
**Other methods**: **Other methods**:
...@@ -352,7 +360,7 @@ You can then simply apply this mixin to a view or viewset anytime you need to ap ...@@ -352,7 +360,7 @@ You can then simply apply this mixin to a view or viewset anytime you need to ap
serializer_class = UserSerializer serializer_class = UserSerializer
lookup_fields = ('account', 'username') lookup_fields = ('account', 'username')
Using custom mixins is a good option if you have custom behavior that needs to be used Using custom mixins is a good option if you have custom behavior that needs to be used.
## Creating custom base classes ## Creating custom base classes
...@@ -368,6 +376,20 @@ If you are using a mixin across multiple views, you can take this a step further ...@@ -368,6 +376,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-existence 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>
---
**Note**: This is the documentation for the **version 3.0** of REST framework. Documentation for [version 2.4](http://tomchristie.github.io/rest-framework-2-docs/) is also available.
---
# 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/
<a class="github" href="pagination.py"></a> source: pagination.py
# Pagination # Pagination
......
<a class="github" href="parsers.py"></a> source: parsers.py
# Parsers # Parsers
...@@ -12,7 +12,7 @@ REST framework includes a number of built in Parser classes, that allow you to a ...@@ -12,7 +12,7 @@ REST framework includes a number of built in Parser classes, that allow you to a
## How the parser is determined ## How the parser is determined
The set of valid parsers for a view is always defined as a list of classes. When either `request.DATA` or `request.FILES` is accessed, REST framework will examine the `Content-Type` header on the incoming request, and determine which parser to use to parse the request content. The set of valid parsers for a view is always defined as a list of classes. When `request.data` is accessed, REST framework will examine the `Content-Type` header on the incoming request, and determine which parser to use to parse the request content.
--- ---
...@@ -48,7 +48,7 @@ using the `APIView` class based views. ...@@ -48,7 +48,7 @@ using the `APIView` class based views.
parser_classes = (YAMLParser,) parser_classes = (YAMLParser,)
def post(self, request, format=None): def post(self, request, format=None):
return Response({'received data': request.DATA}) return Response({'received data': request.data})
Or, if you're using the `@api_view` decorator with function based views. Or, if you're using the `@api_view` decorator with function based views.
...@@ -58,7 +58,7 @@ Or, if you're using the `@api_view` decorator with function based views. ...@@ -58,7 +58,7 @@ Or, if you're using the `@api_view` decorator with function based views.
""" """
A view that can accept POST requests with YAML content. A view that can accept POST requests with YAML content.
""" """
return Response({'received data': request.DATA}) return Response({'received data': request.data})
--- ---
...@@ -92,7 +92,7 @@ Requires the `defusedxml` package to be installed. ...@@ -92,7 +92,7 @@ Requires the `defusedxml` package to be installed.
## FormParser ## FormParser
Parses HTML form content. `request.DATA` will be populated with a `QueryDict` of data, `request.FILES` will be populated with an empty `QueryDict` of data. Parses HTML form content. `request.data` will be populated with a `QueryDict` of data.
You will typically want to use both `FormParser` and `MultiPartParser` together in order to fully support HTML form data. You will typically want to use both `FormParser` and `MultiPartParser` together in order to fully support HTML form data.
...@@ -100,7 +100,7 @@ You will typically want to use both `FormParser` and `MultiPartParser` together ...@@ -100,7 +100,7 @@ You will typically want to use both `FormParser` and `MultiPartParser` together
## MultiPartParser ## MultiPartParser
Parses multipart HTML form content, which supports file uploads. Both `request.DATA` and `request.FILES` will be populated with a `QueryDict`. Parses multipart HTML form content, which supports file uploads. Both `request.data` will be populated with a `QueryDict`.
You will typically want to use both `FormParser` and `MultiPartParser` together in order to fully support HTML form data. You will typically want to use both `FormParser` and `MultiPartParser` together in order to fully support HTML form data.
...@@ -108,7 +108,7 @@ You will typically want to use both `FormParser` and `MultiPartParser` together ...@@ -108,7 +108,7 @@ You will typically want to use both `FormParser` and `MultiPartParser` together
## FileUploadParser ## FileUploadParser
Parses raw file upload content. The `request.DATA` property will be an empty `QueryDict`, and `request.FILES` will be a dictionary with a single key `'file'` containing the uploaded file. Parses raw file upload content. The `request.data` property will be a dictionary with a single key `'file'` containing the uploaded file.
If the view used with `FileUploadParser` is called with a `filename` URL keyword argument, then that argument will be used as the filename. If it is called without a `filename` URL keyword argument, then the client must set the filename in the `Content-Disposition` HTTP header. For example `Content-Disposition: attachment; filename=upload.jpg`. If the view used with `FileUploadParser` is called with a `filename` URL keyword argument, then that argument will be used as the filename. If it is called without a `filename` URL keyword argument, then the client must set the filename in the `Content-Disposition` HTTP header. For example `Content-Disposition: attachment; filename=upload.jpg`.
...@@ -126,7 +126,7 @@ If the view used with `FileUploadParser` is called with a `filename` URL keyword ...@@ -126,7 +126,7 @@ If the view used with `FileUploadParser` is called with a `filename` URL keyword
parser_classes = (FileUploadParser,) parser_classes = (FileUploadParser,)
def put(self, request, filename, format=None): def put(self, request, filename, format=None):
file_obj = request.FILES['file'] file_obj = request.data['file']
# ... # ...
# do some staff with uploaded file # do some staff with uploaded file
# ... # ...
...@@ -139,7 +139,7 @@ If the view used with `FileUploadParser` is called with a `filename` URL keyword ...@@ -139,7 +139,7 @@ If the view used with `FileUploadParser` is called with a `filename` URL keyword
To implement a custom parser, you should override `BaseParser`, set the `.media_type` property, and implement the `.parse(self, stream, media_type, parser_context)` method. To implement a custom parser, you should override `BaseParser`, set the `.media_type` property, and implement the `.parse(self, stream, media_type, parser_context)` method.
The method should return the data that will be used to populate the `request.DATA` property. The method should return the data that will be used to populate the `request.data` property.
The arguments passed to `.parse()` are: The arguments passed to `.parse()` are:
...@@ -161,7 +161,7 @@ By default this will include the following keys: `view`, `request`, `args`, `kwa ...@@ -161,7 +161,7 @@ By default this will include the following keys: `view`, `request`, `args`, `kwa
## Example ## Example
The following is an example plaintext parser that will populate the `request.DATA` property with a string representing the body of the request. The following is an example plaintext parser that will populate the `request.data` property with a string representing the body of the request.
class PlainTextParser(BaseParser): class PlainTextParser(BaseParser):
""" """
......
<a class="github" href="permissions.py"></a> source: permissions.py
# Permissions # Permissions
...@@ -146,7 +146,13 @@ As with `DjangoModelPermissions`, this permission must only be applied to views ...@@ -146,7 +146,13 @@ As with `DjangoModelPermissions`, this permission must only be applied to views
Note that `DjangoObjectPermissions` **does not** require the `django-guardian` package, and should support other object-level backends equally well. Note that `DjangoObjectPermissions` **does not** require the `django-guardian` package, and should support other object-level backends equally well.
As with `DjangoModelPermissions` you can use custom model permissions by overriding `DjangoModelPermissions` and setting the `.perms_map` property. Refer to the source code for details. Note that if you add a custom `view` permission for `GET`, `HEAD` and `OPTIONS` requests, you'll probably also want to consider adding the `DjangoObjectPermissionsFilter` class to ensure that list endpoints only return results including objects for which the user has appropriate view permissions. As with `DjangoModelPermissions` you can use custom model permissions by overriding `DjangoModelPermissions` and setting the `.perms_map` property. Refer to the source code for details.
---
**Note**: If you need object level `view` permissions for `GET`, `HEAD` and `OPTIONS` requests, you'll want to consider also adding the `DjangoObjectPermissionsFilter` class to ensure that list endpoints only return results including objects for which the user has appropriate view permissions.
---
--- ---
...@@ -168,11 +174,7 @@ If you need to test if a request is a read operation or a write operation, you s ...@@ -168,11 +174,7 @@ If you need to test if a request is a read operation or a write operation, you s
--- ---
**Note**: In versions 2.0 and 2.1, the signature for the permission checks always included an optional `obj` parameter, like so: `.has_permission(self, request, view, obj=None)`. The method would be called twice, first for the global permission checks, with no object supplied, and second for the object-level check when required. **Note**: The instance-level `has_object_permission` method will only be called if the view-level `has_permission` checks have already passed. Also note that in order for the instance-level checks to run, the view code should explicitly call `.check_object_permissions(request, obj)`. If you are using the generic views then this will be handled for you by default.
As of version 2.2 this signature has now been replaced with two separate method calls, which is more explicit and obvious. The old style signature continues to work, but its use will result in a `PendingDeprecationWarning`, which is silent by default. In 2.3 this will be escalated to a `DeprecationWarning`, and in 2.4 the old-style signature will be removed.
For more details see the [2.2 release announcement][2.2-announcement].
--- ---
......
<a class="github" href="renderers.py"></a> source: renderers.py
# Renderers # Renderers
...@@ -74,7 +74,7 @@ If your API includes views that can serve both regular webpages and API response ...@@ -74,7 +74,7 @@ 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 the default style is to include unicode characters, and render the response using a compact style with no uneccessary whitespace: 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":"★","value":999} {"unicode black star":"★","value":999}
......
<a class="github" href="request.py"></a> source: request.py
---
**Note**: This is the documentation for the **version 3.0** of REST framework. Documentation for [version 2.4](http://tomchristie.github.io/rest-framework-2-docs/) is also available.
---
# Requests # Requests
...@@ -14,26 +20,29 @@ REST framework's `Request` class extends the standard `HttpRequest`, adding supp ...@@ -14,26 +20,29 @@ REST framework's `Request` class extends the standard `HttpRequest`, adding supp
REST framework's Request objects provide flexible request parsing that allows you to treat requests with JSON data or other media types in the same way that you would normally deal with form data. REST framework's Request objects provide flexible request parsing that allows you to treat requests with JSON data or other media types in the same way that you would normally deal with form data.
## .DATA ## .data
`request.DATA` returns the parsed content of the request body. This is similar to the standard `request.POST` attribute except that: `request.data` returns the parsed content of the request body. This is similar to the standard `request.POST` and `request.FILES` attributes except that:
* It includes all parsed content, including *file and non-file* inputs.
* It supports parsing the content of HTTP methods other than `POST`, meaning that you can access the content of `PUT` and `PATCH` requests. * It supports parsing the content of HTTP methods other than `POST`, meaning that you can access the content of `PUT` and `PATCH` requests.
* It supports REST framework's flexible request parsing, rather than just supporting form data. For example you can handle incoming JSON data in the same way that you handle incoming form data. * It supports REST framework's flexible request parsing, rather than just supporting form data. For example you can handle incoming JSON data in the same way that you handle incoming form data.
For more details see the [parsers documentation]. For more details see the [parsers documentation].
## .FILES ## .query_params
`request.FILES` returns any uploaded files that may be present in the content of the request body. This is the same as the standard `HttpRequest` behavior, except that the same flexible request parsing is used for `request.DATA`. `request.query_params` is a more correctly named synonym for `request.GET`.
For more details see the [parsers documentation]. For clarity inside your code, we recommend using `request.query_params` instead of the Django's standard `request.GET`. Doing so will help keep your codebase more correct and obvious - any HTTP method type may include query parameters, not just `GET` requests.
## .QUERY_PARAMS ## .DATA and .FILES
`request.QUERY_PARAMS` is a more correctly named synonym for `request.GET`. The old-style version 2.x `request.data` and `request.FILES` attributes are still available, but are now pending deprecation in favor of the unified `request.data` attribute.
## .QUERY_PARAMS
For clarity inside your code, we recommend using `request.QUERY_PARAMS` instead of the usual `request.GET`, as *any* HTTP method type may include query parameters. The old-style version 2.x `request.QUERY_PARAMS` attribute is still available, but is now pending deprecation in favor of the more pythonic `request.query_params`.
## .parsers ## .parsers
...@@ -43,12 +52,26 @@ You won't typically need to access this property. ...@@ -43,12 +52,26 @@ You won't typically need to access this property.
--- ---
**Note:** If a client sends malformed content, then accessing `request.DATA` or `request.FILES` may raise a `ParseError`. By default REST framework's `APIView` class or `@api_view` decorator will catch the error and return a `400 Bad Request` response. **Note:** If a client sends malformed content, then accessing `request.data` may raise a `ParseError`. By default REST framework's `APIView` class or `@api_view` decorator will catch the error and return a `400 Bad Request` response.
If a client sends a request with a content-type that cannot be parsed then a `UnsupportedMediaType` exception will be raised, which by default will be caught and return a `415 Unsupported Media Type` response. If a client sends a request with a content-type that cannot be parsed then a `UnsupportedMediaType` exception will be raised, which by default will be caught and return a `415 Unsupported Media Type` response.
--- ---
# Content negotiation
The request exposes some properties that allow you to determine the result of the content negotiation stage. This allows you to implement behaviour such as selecting a different serialisation schemes for different media types.
## .accepted_renderer
The renderer instance what was selected by the content negotiation stage.
## .accepted_media_type
A string representing the media type that was accepted by the content negotiation stage.
---
# Authentication # Authentication
REST framework provides flexible, per-request authentication, that gives you the ability to: REST framework provides flexible, per-request authentication, that gives you the ability to:
......
<a class="github" href="response.py"></a> source: response.py
# Responses # Responses
......
<a class="github" href="reverse.py"></a> source: reverse.py
# Returning URLs # Returning URLs
......
<a class="github" href="routers.py"></a> source: routers.py
# Routers # Routers
......
<a class="github" href="settings.py"></a> source: settings.py
# Settings # Settings
...@@ -51,7 +51,7 @@ Default: ...@@ -51,7 +51,7 @@ Default:
#### DEFAULT_PARSER_CLASSES #### DEFAULT_PARSER_CLASSES
A list or tuple of parser classes, that determines the default set of parsers used when accessing the `request.DATA` property. A list or tuple of parser classes, that determines the default set of parsers used when accessing the `request.data` property.
Default: Default:
...@@ -74,7 +74,7 @@ Default: ...@@ -74,7 +74,7 @@ Default:
#### DEFAULT_PERMISSION_CLASSES #### DEFAULT_PERMISSION_CLASSES
A list or tuple of permission classes, that determines the default set of permissions checked at the start of a view. A list or tuple of permission classes, that determines the default set of permissions checked at the start of a view. Permission must be granted by every class in the list.
Default: Default:
...@@ -154,13 +154,13 @@ Default: `None` ...@@ -154,13 +154,13 @@ Default: `None`
### SEARCH_PARAM ### SEARCH_PARAM
The name of a query paramater, which can be used to specify the search term used by `SearchFilter`. The name of a query parameter, which can be used to specify the search term used by `SearchFilter`.
Default: `search` Default: `search`
#### ORDERING_PARAM #### ORDERING_PARAM
The name of a query paramater, which can be used to specify the ordering of results returned by `OrderingFilter`. The name of a query parameter, which can be used to specify the ordering of results returned by `OrderingFilter`.
Default: `ordering` Default: `ordering`
...@@ -321,7 +321,7 @@ When set to `False`, JSON responses will escape non-ascii characters, like so: ...@@ -321,7 +321,7 @@ When set to `False`, JSON responses will escape non-ascii characters, like so:
{"unicode black star":"\u2605"} {"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. Both styles conform to [RFC 4627][rfc4627], and are syntactically valid JSON. The unicode style is preferred as being more user-friendly when inspecting API responses.
Default: `True` Default: `True`
...@@ -399,6 +399,12 @@ This should be a function with the following signature: ...@@ -399,6 +399,12 @@ This should be a function with the following signature:
Default: `'rest_framework.views.exception_handler'` Default: `'rest_framework.views.exception_handler'`
#### NON_FIELD_ERRORS_KEY
A string representing the key that should be used for serializer errors that do not refer to a specific field, but are instead general errors.
Default: `'non_field_errors'`
#### URL_FIELD_NAME #### URL_FIELD_NAME
A string representing the key that should be used for the URL fields generated by `HyperlinkedModelSerializer`. A string representing the key that should be used for the URL fields generated by `HyperlinkedModelSerializer`.
......
<a class="github" href="status.py"></a> source: status.py
# Status Codes # Status Codes
......
<a class="github" href="test.py"></a> source: test.py
# Testing # Testing
......
<a class="github" href="throttling.py"></a> source: throttling.py
# Throttling # Throttling
...@@ -74,7 +74,7 @@ If you need to strictly identify unique client IP addresses, you'll need to firs ...@@ -74,7 +74,7 @@ If you need to strictly identify unique client IP addresses, you'll need to firs
It is important to understand that if you configure the `NUM_PROXIES` setting, then all clients behind a unique [NAT'd](http://en.wikipedia.org/wiki/Network_address_translation) gateway will be treated as a single client. It is important to understand that if you configure the `NUM_PROXIES` setting, then all clients behind a unique [NAT'd](http://en.wikipedia.org/wiki/Network_address_translation) gateway will be treated as a single client.
Further context on how the `X-Forwarded-For` header works, and identifing a remote client IP can be [found here][identifing-clients]. Further context on how the `X-Forwarded-For` header works, and identifying a remote client IP can be [found here][identifing-clients].
## Setting up the cache ## Setting up the cache
...@@ -85,7 +85,7 @@ If you need to use a cache other than `'default'`, you can do so by creating a c ...@@ -85,7 +85,7 @@ If you need to use a cache other than `'default'`, you can do so by creating a c
class CustomAnonRateThrottle(AnonRateThrottle): class CustomAnonRateThrottle(AnonRateThrottle):
cache = get_cache('alternate') cache = get_cache('alternate')
You'll need to rememeber to also set your custom throttle class in the `'DEFAULT_THROTTLE_CLASSES'` settings key, or using the `throttle_classes` view attribute. You'll need to remember to also set your custom throttle class in the `'DEFAULT_THROTTLE_CLASSES'` settings key, or using the `throttle_classes` view attribute.
--- ---
......
<a class="github" href="decorators.py"></a> <a class="github" href="views.py"></a> source: decorators.py
views.py
# Class Based Views # Class Based Views
......
<a class="github" href="viewsets.py"></a> source: viewsets.py
# ViewSets # ViewSets
...@@ -124,7 +124,7 @@ For example: ...@@ -124,7 +124,7 @@ For example:
@detail_route(methods=['post']) @detail_route(methods=['post'])
def set_password(self, request, pk=None): def set_password(self, request, pk=None):
user = self.get_object() user = self.get_object()
serializer = PasswordSerializer(data=request.DATA) serializer = PasswordSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
user.set_password(serializer.data['password']) user.set_password(serializer.data['password'])
user.save() user.save()
......
...@@ -192,6 +192,18 @@ body{ ...@@ -192,6 +192,18 @@ body{
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{ .navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
color: #A30000; color: #A30000;
} }
.dropdown-menu .active > a,
.dropdown-menu .active > a:hover {
background-image: none;
}
.navbar-inverse .nav .dropdown .active > a,
.navbar-inverse .nav .dropdown .active > a:hover,
.navbar-inverse .nav .dropdown .active > a:focus {
background-color: #eeeeee;
}
.navbar .navbar-inner .dropdown-menu li a:hover{ .navbar .navbar-inner .dropdown-menu li a:hover{
background: #eeeeee; background: #eeeeee;
color: #c20000; color: #c20000;
......
...@@ -9,6 +9,10 @@ ...@@ -9,6 +9,10 @@
--- ---
**Note**: This is the documentation for the **version 3.0** of REST framework. Documentation for [version 2.4](http://tomchristie.github.io/rest-framework-2-docs/) is also available.
---
<p> <p>
<h1 style="position: absolute; <h1 style="position: absolute;
width: 1px; width: 1px;
...@@ -22,9 +26,6 @@ ...@@ -22,9 +26,6 @@
<img alt="Django REST Framework" title="Logo by Jake 'Sid' Smith" src="img/logo.png" width="600px" style="display: block; margin: 0 auto 0 auto"> <img alt="Django REST Framework" title="Logo by Jake 'Sid' Smith" src="img/logo.png" width="600px" style="display: block; margin: 0 auto 0 auto">
</p> </p>
<!--
# Django REST framework
-->
Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs. Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs.
...@@ -43,14 +44,12 @@ Some reasons you might want to use REST framework: ...@@ -43,14 +44,12 @@ Some reasons you might want to use REST framework:
**Above**: *Screenshot from the browsable API* **Above**: *Screenshot from the browsable API*
----
## Requirements ## Requirements
REST framework requires the following: REST framework requires the following:
* Python (2.6.5+, 2.7, 3.2, 3.3) * 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:
...@@ -82,10 +81,10 @@ Add `'rest_framework'` to your `INSTALLED_APPS` setting. ...@@ -82,10 +81,10 @@ Add `'rest_framework'` to your `INSTALLED_APPS` setting.
If you're intending to use the browsable API you'll probably also want to add REST framework's login and logout views. Add the following to your root `urls.py` file. If you're intending to use the browsable API you'll probably also want to add REST framework's login and logout views. Add the following to your root `urls.py` file.
urlpatterns = patterns('', urlpatterns = [
... ...
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
) ]
Note that the URL path can be whatever you want, but you must include `'rest_framework.urls'` with the `'rest_framework'` namespace. Note that the URL path can be whatever you want, but you must include `'rest_framework.urls'` with the `'rest_framework'` namespace.
...@@ -170,6 +169,7 @@ The API guide is your complete reference manual to all the functionality provide ...@@ -170,6 +169,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]
...@@ -289,6 +289,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -289,6 +289,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
[validators]: api-guide/validators.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
......
...@@ -15,7 +15,7 @@ As an example of just how simple REST framework APIs can now be, here's an API w ...@@ -15,7 +15,7 @@ As an example of just how simple REST framework APIs can now be, here's an API w
""" """
A REST framework API for viewing and editing users and groups. A REST framework API for viewing and editing users and groups.
""" """
from django.conf.urls.defaults import url, patterns, include from django.conf.urls.defaults import url, include
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from rest_framework import viewsets, routers from rest_framework import viewsets, routers
...@@ -36,10 +36,10 @@ As an example of just how simple REST framework APIs can now be, here's an API w ...@@ -36,10 +36,10 @@ As an example of just how simple REST framework APIs can now be, here's an API w
# Wire up our API using automatic URL routing. # Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browseable API. # Additionally, we include login URLs for the browseable API.
urlpatterns = patterns('', urlpatterns = [
url(r'^', include(router.urls)), url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
) ]
The best place to get started with ViewSets and Routers is to take a look at the [newest section in the tutorial][part-6], which demonstrates their usage. The best place to get started with ViewSets and Routers is to take a look at the [newest section in the tutorial][part-6], which demonstrates their usage.
......
...@@ -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.
...@@ -164,8 +164,8 @@ Once again, many thanks to all the generous [backers and sponsors][kickstarter-s ...@@ -164,8 +164,8 @@ Once again, many thanks to all the generous [backers and sponsors][kickstarter-s
[lts-releases]: https://docs.djangoproject.com/en/dev/internals/release-process/#long-term-support-lts-releases [lts-releases]: https://docs.djangoproject.com/en/dev/internals/release-process/#long-term-support-lts-releases
[2-4-release-notes]: release-notes#240 [2-4-release-notes]: release-notes#240
[view-name-and-description-settings]: ../api-guide/settings/#view-names-and-descriptions [view-name-and-description-settings]: ../api-guide/settings#view-names-and-descriptions
[client-ip-identification]: ../api-guide/throttling/#how-clients-are-identified [client-ip-identification]: ../api-guide/throttling#how-clients-are-identified
[2-3-announcement]: 2.3-announcement [2-3-announcement]: 2.3-announcement
[github-labels]: https://github.com/tomchristie/django-rest-framework/issues [github-labels]: https://github.com/tomchristie/django-rest-framework/issues
[github-milestones]: https://github.com/tomchristie/django-rest-framework/milestones [github-milestones]: https://github.com/tomchristie/django-rest-framework/milestones
......
...@@ -35,7 +35,7 @@ The best way to deal with CORS in REST framework is to add the required response ...@@ -35,7 +35,7 @@ The best way to deal with CORS in REST framework is to add the required response
[cite]: http://www.codinghorror.com/blog/2008/10/preventing-csrf-and-xsrf-attacks.html [cite]: http://www.codinghorror.com/blog/2008/10/preventing-csrf-and-xsrf-attacks.html
[csrf]: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF) [csrf]: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)
[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax [csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax
[cors]: http://www.w3.org/TR/cors/ [cors]: http://www.w3.org/TR/cors/
[ottoyiu]: https://github.com/ottoyiu/ [ottoyiu]: https://github.com/ottoyiu/
[django-cors-headers]: https://github.com/ottoyiu/django-cors-headers/ [django-cors-headers]: https://github.com/ottoyiu/django-cors-headers/
...@@ -130,34 +130,24 @@ You can override the `BrowsableAPIRenderer.get_context()` method to customise th ...@@ -130,34 +130,24 @@ You can override the `BrowsableAPIRenderer.get_context()` method to customise th
For more advanced customization, such as not having a Bootstrap basis or tighter integration with the rest of your site, you can simply choose not to have `api.html` extend `base.html`. Then the page content and capabilities are entirely up to you. For more advanced customization, such as not having a Bootstrap basis or tighter integration with the rest of your site, you can simply choose not to have `api.html` extend `base.html`. Then the page content and capabilities are entirely up to you.
#### Autocompletion #### Handling `ChoiceField` with large numbers of items.
When a `ChoiceField` has too many items, rendering the widget containing all the options can become very slow, and cause the browsable API rendering to perform poorly. One solution is to replace the selector by an autocomplete widget, that only loads and renders a subset of the available options as needed. When a relationship or `ChoiceField` has too many items, rendering the widget containing all the options can become very slow, and cause the browsable API rendering to perform poorly.
There are [a variety of packages for autocomplete widgets][autocomplete-packages], such as [django-autocomplete-light][django-autocomplete-light]. To setup `django-autocomplete-light`, follow the [installation documentation][django-autocomplete-light-install], add the the following to the `api.html` template: The simplest option in this case is to replace the select input with a standard text input. For example:
{% block script %} author = serializers.HyperlinkedRelatedField(
{{ block.super }} queryset=User.objects.all(),
{% include 'autocomplete_light/static.html' %} style={'base_template': 'input.html'}
{% endblock %}
You can now add the `autocomplete_light.ChoiceWidget` widget to the serializer field.
import autocomplete_light
class BookSerializer(serializers.ModelSerializer):
author = serializers.ChoiceField(
widget=autocomplete_light.ChoiceWidget('AuthorAutocomplete')
) )
class Meta: #### Autocomplete
model = Book
--- An alternative, but more complex option would be to replace the input with an autocomplete widget, that only loads and renders a subset of the available options as needed. If you need to do this you'll need to do some work to build a custom autocomplete HTML template yourself.
![Autocomplete][autocomplete-image] There are [a variety of packages for autocomplete widgets][autocomplete-packages], such as [django-autocomplete-light][django-autocomplete-light], that you may want to refer to. Note that you will not be able to simply include these components as standard widgets, but will need to write the HTML template explicitly. This is because REST framework 3.0 no longer supports the `widget` keyword argument since it now uses templated HTML generation.
*Screenshot of the autocomplete-light widget* Better support for autocomplete inputs is planned in future versions.
--- ---
...@@ -175,4 +165,3 @@ You can now add the `autocomplete_light.ChoiceWidget` widget to the serializer f ...@@ -175,4 +165,3 @@ You can now add the `autocomplete_light.ChoiceWidget` widget to the serializer f
[autocomplete-packages]: https://www.djangopackages.com/grids/g/auto-complete/ [autocomplete-packages]: https://www.djangopackages.com/grids/g/auto-complete/
[django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light [django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light
[django-autocomplete-light-install]: http://django-autocomplete-light.readthedocs.org/en/latest/#install [django-autocomplete-light-install]: http://django-autocomplete-light.readthedocs.org/en/latest/#install
[autocomplete-image]: ../img/autocomplete.png
...@@ -10,7 +10,7 @@ Minor version numbers (0.0.x) are used for changes that are API compatible. You ...@@ -10,7 +10,7 @@ Minor version numbers (0.0.x) are used for changes that are API compatible. You
Medium version numbers (0.x.0) may include API changes, in line with the [deprecation policy][deprecation-policy]. You should read the release notes carefully before upgrading between medium point releases. Medium version numbers (0.x.0) may include API changes, in line with the [deprecation policy][deprecation-policy]. You should read the release notes carefully before upgrading between medium point releases.
Major version numbers (x.0.0) are reserved for substantial project milestones. No major point releases are currently planned. Major version numbers (x.0.0) are reserved for substantial project milestones.
## Deprecation policy ## Deprecation policy
...@@ -40,21 +40,47 @@ You can determine your currently installed version using `pip freeze`: ...@@ -40,21 +40,47 @@ You can determine your currently installed version using `pip freeze`:
## 2.4.x series ## 2.4.x series
### 2.4.4
**Date**: [3rd November 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.4+Release%22+).
* **Security fix**: Escape URLs when replacing `format=` query parameter, as used in dropdown on `GET` button in browsable API to allow explicit selection of JSON vs HTML output.
* Maintain ordering of URLs in API root view for `DefaultRouter`.
* Fix `follow=True` in `APIRequestFactory`
* Resolve issue with invalid `read_only=True`, `required=True` fields being automatically generated by `ModelSerializer` in some cases.
* Resolve issue with `OPTIONS` requests returning incorrect information for views using `get_serializer_class` to dynamically determine serializer based on request method.
### 2.4.3
**Date**: [19th September 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.3+Release%22+).
* Support translatable view docstrings being displayed in the browsable API.
* Support [encoded `filename*`][rfc-6266] in raw file uploads with `FileUploadParser`.
* Allow routers to support viewsets that don't include any list routes or that don't include any detail routes.
* Don't render an empty login control in browsable API if `login` view is not included.
* CSRF exemption performed in `.as_view()` to prevent accidental omission if overriding `.dispatch()`.
* Login on browsable API now displays validation errors.
* Bugfix: Fix migration in `authtoken` application.
* Bugfix: Allow selection of integer keys in nested choices.
* Bugfix: Return `None` instead of `'None'` in `CharField` with `allow_none=True`.
* Bugfix: Ensure custom model fields map to equivelent serializer fields more reliably.
* Bugfix: `DjangoFilterBackend` no longer quietly changes queryset ordering.
### 2.4.2 ### 2.4.2
**Date**: 3rd September 2014 **Date**: [3rd September 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.2+Release%22+).
* Bugfix: Fix broken pagination for 2.4.x series. * Bugfix: Fix broken pagination for 2.4.x series.
### 2.4.1 ### 2.4.1
**Date**: 1st September 2014 **Date**: [1st September 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.1+Release%22+).
* Bugfix: Fix broken login template for browsable API. * Bugfix: Fix broken login template for browsable API.
### 2.4.0 ### 2.4.0
**Date**: 29th August 2014 **Date**: [29th August 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.0+Release%22+).
**Django version requirements**: The lowest supported version of Django is now 1.4.2. **Django version requirements**: The lowest supported version of Django is now 1.4.2.
...@@ -133,7 +159,7 @@ You can determine your currently installed version using `pip freeze`: ...@@ -133,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.
...@@ -416,7 +442,7 @@ The security vulnerabilities only affect APIs which use the `XMLParser` class, b ...@@ -416,7 +442,7 @@ The security vulnerabilities only affect APIs which use the `XMLParser` class, b
* Bugfix: Validation errors instead of exceptions when related fields receive incorrect types. * Bugfix: Validation errors instead of exceptions when related fields receive incorrect types.
* Bugfix: Handle ObjectDoesNotExist exception when serializing null reverse one-to-one * Bugfix: Handle ObjectDoesNotExist exception when serializing null reverse one-to-one
**Note**: Prior to 2.1.16, The Decimals would render in JSON using floating point if `simplejson` was installed, but otherwise render using string notation. Now that use of `simplejson` has been deprecated, Decimals will consistently render using string notation. See [#582] for more details. **Note**: Prior to 2.1.16, The Decimals would render in JSON using floating point if `simplejson` was installed, but otherwise render using string notation. Now that use of `simplejson` has been deprecated, Decimals will consistently render using string notation. See [ticket 582](ticket-582) for more details.
### 2.1.15 ### 2.1.15
...@@ -588,122 +614,7 @@ This change will not affect user code, so long as it's following the recommended ...@@ -588,122 +614,7 @@ This change will not affect user code, so long as it's following the recommended
* **Fix all of the things.** (Well, almost.) * **Fix all of the things.** (Well, almost.)
* For more information please see the [2.0 announcement][announcement]. * For more information please see the [2.0 announcement][announcement].
--- For older release notes, [please see the GitHub repo](old-release-notes).
## 0.4.x series
### 0.4.0
* Supports Django 1.5.
* Fixes issues with 'HEAD' method.
* Allow views to specify template used by TemplateRenderer
* More consistent error responses
* Some serializer fixes
* Fix internet explorer ajax behavior
* Minor xml and yaml fixes
* Improve setup (e.g. use staticfiles, not the defunct ADMIN_MEDIA_PREFIX)
* Sensible absolute URL generation, not using hacky set_script_prefix
---
## 0.3.x series
### 0.3.3
* Added DjangoModelPermissions class to support `django.contrib.auth` style permissions.
* Use `staticfiles` for css files.
- Easier to override. Won't conflict with customized admin styles (e.g. grappelli)
* Templates are now nicely namespaced.
- Allows easier overriding.
* Drop implied 'pk' filter if last arg in urlconf is unnamed.
- Too magical. Explicit is better than implicit.
* Saner template variable auto-escaping.
* Tidier setup.py
* Updated for URLObject 2.0
* Bugfixes:
- Bug with PerUserThrottling when user contains unicode chars.
### 0.3.2
* Bugfixes:
* Fix 403 for POST and PUT from the UI with UserLoggedInAuthentication (#115)
* serialize_model method in serializer.py may cause wrong value (#73)
* Fix Error when clicking OPTIONS button (#146)
* And many other fixes
* Remove short status codes
- Zen of Python: "There should be one-- and preferably only one --obvious way to do it."
* get_name, get_description become methods on the view - makes them overridable.
* Improved model mixin API - Hooks for build_query, get_instance_data, get_model, get_queryset, get_ordering
### 0.3.1
* [not documented]
### 0.3.0
* JSONP Support
* Bugfixes, including support for latest markdown release
---
## 0.2.x series
### 0.2.4
* Fix broken IsAdminUser permission.
* OPTIONS support.
* XMLParser.
* Drop mentions of Blog, BitBucket.
### 0.2.3
* Fix some throttling bugs.
* ``X-Throttle`` header on throttling.
* Support for nesting resources on related models.
### 0.2.2
* Throttling support complete.
### 0.2.1
* Couple of simple bugfixes over 0.2.0
### 0.2.0
* Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear.
The public API has been massively cleaned up. Expect it to be fairly stable from here on in.
* ``Resource`` becomes decoupled into ``View`` and ``Resource``, your views should now inherit from ``View``, not ``Resource``.
* The handler functions on views ``.get() .put() .post()`` etc, no longer have the ``content`` and ``auth`` args.
Use ``self.CONTENT`` inside a view to access the deserialized, validated content.
Use ``self.user`` inside a view to access the authenticated user.
* ``allowed_methods`` and ``anon_allowed_methods`` are now defunct. if a method is defined, it's available.
The ``permissions`` attribute on a ``View`` is now used to provide generic permissions checking.
Use permission classes such as ``FullAnonAccess``, ``IsAuthenticated`` or ``IsUserOrIsAnonReadOnly`` to set the permissions.
* The ``authenticators`` class becomes ``authentication``. Class names change to ``Authentication``.
* The ``emitters`` class becomes ``renderers``. Class names change to ``Renderers``.
* ``ResponseException`` becomes ``ErrorResponse``.
* The mixin classes have been nicely refactored, the basic mixins are now ``RequestMixin``, ``ResponseMixin``, ``AuthMixin``, and ``ResourceMixin``
You can reuse these mixin classes individually without using the ``View`` class.
---
## 0.1.x series
### 0.1.1
* Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1.
### 0.1.0
* Initial release.
[cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html [cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html
[deprecation-policy]: #deprecation-policy [deprecation-policy]: #deprecation-policy
...@@ -716,4 +627,6 @@ This change will not affect user code, so long as it's following the recommended ...@@ -716,4 +627,6 @@ This change will not affect user code, so long as it's following the recommended
[staticfiles13]: https://docs.djangoproject.com/en/1.3/howto/static-files/#with-a-template-tag [staticfiles13]: https://docs.djangoproject.com/en/1.3/howto/static-files/#with-a-template-tag
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion [2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
[announcement]: rest-framework-2-announcement.md [announcement]: rest-framework-2-announcement.md
[#582]: https://github.com/tomchristie/django-rest-framework/issues/582 [ticket-582]: https://github.com/tomchristie/django-rest-framework/issues/582
[rfc-6266]: http://tools.ietf.org/html/rfc6266#section-4.3
[old-release-notes]: https://github.com/tomchristie/django-rest-framework/blob/2.4.4/docs/topics/release-notes.md#04x-series
...@@ -4,16 +4,16 @@ ...@@ -4,16 +4,16 @@
> >
> &mdash; Mike Amundsen, [REST fest 2012 keynote][cite]. > &mdash; Mike Amundsen, [REST fest 2012 keynote][cite].
First off, the disclaimer. The name "Django REST framework" was chosen simply to sure the project would be easily found by developers. Throughout the documentation we try to use the more simple and technically correct terminology of "Web APIs". First off, the disclaimer. The name "Django REST framework" was decided back in early 2011 and was chosen simply to sure the project would be easily found by developers. Throughout the documentation we try to use the more simple and technically correct terminology of "Web APIs".
If you are serious about designing a Hypermedia APIs, you should look to resources outside of this documentation to help inform your design choices. If you are serious about designing a Hypermedia API, you should look to resources outside of this documentation to help inform your design choices.
The following fall into the "required reading" category. The following fall into the "required reading" category.
* Roy Fielding's dissertation - [Architectural Styles and * Roy Fielding's dissertation - [Architectural Styles and
the Design of Network-based Software Architectures][dissertation]. the Design of Network-based Software Architectures][dissertation].
* Roy Fielding's "[REST APIs must be hypertext-driven][hypertext-driven]" blog post. * Roy Fielding's "[REST APIs must be hypertext-driven][hypertext-driven]" blog post.
* Leonard Richardson & Sam Ruby's [RESTful Web Services][restful-web-services]. * Leonard Richardson & Mike Amundsen's [RESTful Web APIs][restful-web-apis].
* Mike Amundsen's [Building Hypermedia APIs with HTML5 and Node][building-hypermedia-apis]. * Mike Amundsen's [Building Hypermedia APIs with HTML5 and Node][building-hypermedia-apis].
* Steve Klabnik's [Designing Hypermedia APIs][designing-hypermedia-apis]. * Steve Klabnik's [Designing Hypermedia APIs][designing-hypermedia-apis].
* The [Richardson Maturity Model][maturitymodel]. * The [Richardson Maturity Model][maturitymodel].
...@@ -37,7 +37,7 @@ What REST framework doesn't do is give you is machine readable hypermedia format ...@@ -37,7 +37,7 @@ What REST framework doesn't do is give you is machine readable hypermedia format
[cite]: http://vimeo.com/channels/restfest/page:2 [cite]: http://vimeo.com/channels/restfest/page:2
[dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm [dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
[hypertext-driven]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven [hypertext-driven]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
[restful-web-services]: http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/0596529260 [restful-web-apis]: http://restfulwebapis.org/
[building-hypermedia-apis]: http://www.amazon.com/Building-Hypermedia-APIs-HTML5-Node/dp/1449306578 [building-hypermedia-apis]: http://www.amazon.com/Building-Hypermedia-APIs-HTML5-Node/dp/1449306578
[designing-hypermedia-apis]: http://designinghypermediaapis.com/ [designing-hypermedia-apis]: http://designinghypermediaapis.com/
[restisover]: http://blog.steveklabnik.com/posts/2012-02-23-rest-is-over [restisover]: http://blog.steveklabnik.com/posts/2012-02-23-rest-is-over
......
...@@ -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 = (
... ...
...@@ -64,15 +51,15 @@ We'll also need to add our new `snippets` app and the `rest_framework` app to `I ...@@ -64,15 +51,15 @@ We'll also need to add our new `snippets` app and the `rest_framework` app to `I
We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs. We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs.
urlpatterns = patterns('', urlpatterns = [
url(r'^', include('snippets.urls')), url(r'^', include('snippets.urls')),
) ]
Okay, we're ready to roll. 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')
One nice property that serializers have is that you can inspect all the fields in a 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 magical, they are simply a shortcut for creating serializer classes:
* 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.
...@@ -297,11 +302,12 @@ We'll also need a view which corresponds to an individual snippet, and can be us ...@@ -297,11 +302,12 @@ We'll also need a view which corresponds to an individual snippet, and can be us
Finally we need to wire these views up. Create the `snippets/urls.py` file: Finally we need to wire these views up. Create the `snippets/urls.py` file:
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from snippets import views
urlpatterns = patterns('snippets.views', urlpatterns = [
url(r'^snippets/$', 'snippet_list'), url(r'^snippets/$', views.snippet_list),
url(r'^snippets/(?P<pk>[0-9]+)/$', 'snippet_detail'), url(r'^snippets/(?P<pk>[0-9]+)/$', views.snippet_detail),
) ]
It's worth noting that there are a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now. It's worth noting that there are a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now.
......
...@@ -5,10 +5,10 @@ Let's introduce a couple of essential building blocks. ...@@ -5,10 +5,10 @@ Let's introduce a couple of essential building blocks.
## Request objects ## Request objects
REST framework introduces a `Request` object that extends the regular `HttpRequest`, and provides more flexible request parsing. The core functionality of the `Request` object is the `request.DATA` attribute, which is similar to `request.POST`, but more useful for working with Web APIs. REST framework introduces a `Request` object that extends the regular `HttpRequest`, and provides more flexible request parsing. The core functionality of the `Request` object is the `request.data` attribute, which is similar to `request.POST`, but more useful for working with Web APIs.
request.POST # Only handles form data. Only works for 'POST' method. request.POST # Only handles form data. Only works for 'POST' method.
request.DATA # Handles arbitrary data. Works for 'POST', 'PUT' and 'PATCH' methods. request.data # Handles arbitrary data. Works for 'POST', 'PUT' and 'PATCH' methods.
## Response objects ## Response objects
...@@ -29,7 +29,7 @@ REST framework provides two wrappers you can use to write API views. ...@@ -29,7 +29,7 @@ REST framework provides two wrappers you can use to write API views.
These wrappers provide a few bits of functionality such as making sure you receive `Request` instances in your view, and adding context to `Response` objects so that content negotiation can be performed. These wrappers provide a few bits of functionality such as making sure you receive `Request` instances in your view, and adding context to `Response` objects so that content negotiation can be performed.
The wrappers also provide behaviour such as returning `405 Method Not Allowed` responses when appropriate, and handling any `ParseError` exception that occurs when accessing `request.DATA` with malformed input. The wrappers also provide behaviour such as returning `405 Method Not Allowed` responses when appropriate, and handling any `ParseError` exception that occurs when accessing `request.data` with malformed input.
## Pulling it all together ## Pulling it all together
...@@ -55,7 +55,7 @@ We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and de ...@@ -55,7 +55,7 @@ We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and de
return Response(serializer.data) return Response(serializer.data)
elif request.method == 'POST': elif request.method == 'POST':
serializer = SnippetSerializer(data=request.DATA) serializer = SnippetSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
...@@ -80,7 +80,7 @@ Here is the view for an individual snippet, in the `views.py` module. ...@@ -80,7 +80,7 @@ Here is the view for an individual snippet, in the `views.py` module.
return Response(serializer.data) return Response(serializer.data)
elif request.method == 'PUT': elif request.method == 'PUT':
serializer = SnippetSerializer(snippet, data=request.DATA) serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data) return Response(serializer.data)
...@@ -92,7 +92,7 @@ Here is the view for an individual snippet, in the `views.py` module. ...@@ -92,7 +92,7 @@ Here is the view for an individual snippet, in the `views.py` module.
This should all feel very familiar - it is not a lot different from working with regular Django views. This should all feel very familiar - it is not a lot different from working with regular Django views.
Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.DATA` can handle incoming `json` requests, but it can also handle `yaml` and other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us. Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.data` can handle incoming `json` requests, but it can also handle `yaml` and other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us.
## Adding optional format suffixes to our URLs ## Adding optional format suffixes to our URLs
...@@ -110,11 +110,12 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter ...@@ -110,11 +110,12 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
urlpatterns = patterns('snippets.views', urlpatterns = [
url(r'^snippets/$', 'snippet_list'), url(r'^snippets/$', views.snippet_list),
url(r'^snippets/(?P<pk>[0-9]+)$', 'snippet_detail'), url(r'^snippets/(?P<pk>[0-9]+)$', views.snippet_detail),
) ]
urlpatterns = format_suffix_patterns(urlpatterns) urlpatterns = format_suffix_patterns(urlpatterns)
......
...@@ -24,7 +24,7 @@ We'll start by rewriting the root view as a class based view. All this involves ...@@ -24,7 +24,7 @@ We'll start by rewriting the root view as a class based view. All this involves
return Response(serializer.data) return Response(serializer.data)
def post(self, request, format=None): def post(self, request, format=None):
serializer = SnippetSerializer(data=request.DATA) serializer = SnippetSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
...@@ -49,7 +49,7 @@ So far, so good. It looks pretty similar to the previous case, but we've got be ...@@ -49,7 +49,7 @@ So far, so good. It looks pretty similar to the previous case, but we've got be
def put(self, request, pk, format=None): def put(self, request, pk, format=None):
snippet = self.get_object(pk) snippet = self.get_object(pk)
serializer = SnippetSerializer(snippet, data=request.DATA) serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data) return Response(serializer.data)
...@@ -68,10 +68,10 @@ We'll also need to refactor our `urls.py` slightly now we're using class based v ...@@ -68,10 +68,10 @@ We'll also need to refactor our `urls.py` slightly now we're using class based v
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views from snippets import views
urlpatterns = patterns('', urlpatterns = [
url(r'^snippets/$', views.SnippetList.as_view()), url(r'^snippets/$', views.SnippetList.as_view()),
url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view()), url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view()),
) ]
urlpatterns = format_suffix_patterns(urlpatterns) urlpatterns = format_suffix_patterns(urlpatterns)
......
...@@ -44,7 +44,9 @@ When that's all done we'll need to update our database tables. ...@@ -44,7 +44,9 @@ When that's all done we'll need to update our database tables.
Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again. Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again.
rm tmp.db rm tmp.db
python manage.py syncdb rm -r snippets/migrations
python manage.py makemigrations snippets
python manage.py migrate
You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with the `createsuperuser` command. You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with the `createsuperuser` command.
...@@ -92,24 +94,26 @@ Finally we need to add those views into the API, by referencing them from the UR ...@@ -92,24 +94,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
...@@ -137,10 +141,10 @@ Add the following import at the top of the file: ...@@ -137,10 +141,10 @@ Add the following import at the top of the file:
And, at the end of the file, add a pattern to include the login and logout views for the browsable API. And, at the end of the file, add a pattern to include the login and logout views for the browsable API.
urlpatterns += patterns('', urlpatterns += [
url(r'^api-auth/', include('rest_framework.urls', url(r'^api-auth/', include('rest_framework.urls',
namespace='rest_framework')), namespace='rest_framework')),
) ]
The `r'^api-auth/'` part of pattern can actually be whatever URL you want to use. The only restriction is that the included urls must use the `'rest_framework'` namespace. The `r'^api-auth/'` part of pattern can actually be whatever URL you want to use. The only restriction is that the included urls must use the `'rest_framework'` namespace.
......
...@@ -6,7 +6,6 @@ At the moment relationships within our API are represented by using primary keys ...@@ -6,7 +6,6 @@ At the moment relationships within our API are represented by using primary keys
Right now we have endpoints for 'snippets' and 'users', but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and the `@api_view` decorator we introduced earlier. In your `snippets/views.py` add: Right now we have endpoints for 'snippets' and 'users', but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and the `@api_view` decorator we introduced earlier. In your `snippets/views.py` add:
from rest_framework import renderers
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
...@@ -108,8 +107,8 @@ If we're going to have a hyperlinked API, we need to make sure we name our URL p ...@@ -108,8 +107,8 @@ If we're going to have a hyperlinked API, we need to make sure we name our URL p
After adding all those names into our URLconf, our final `snippets/urls.py` file should look something like this: After adding all those names into our URLconf, our final `snippets/urls.py` file should look something like this:
# API endpoints # API endpoints
urlpatterns = format_suffix_patterns(patterns('snippets.views', urlpatterns = format_suffix_patterns([
url(r'^$', 'api_root'), url(r'^$', views.api_root),
url(r'^snippets/$', url(r'^snippets/$',
views.SnippetList.as_view(), views.SnippetList.as_view(),
name='snippet-list'), name='snippet-list'),
...@@ -125,13 +124,13 @@ After adding all those names into our URLconf, our final `snippets/urls.py` file ...@@ -125,13 +124,13 @@ After adding all those names into our URLconf, our final `snippets/urls.py` file
url(r'^users/(?P<pk>[0-9]+)/$', url(r'^users/(?P<pk>[0-9]+)/$',
views.UserDetail.as_view(), views.UserDetail.as_view(),
name='user-detail') name='user-detail')
)) ])
# Login and logout views for the browsable API # Login and logout views for the browsable API
urlpatterns += patterns('', urlpatterns += [
url(r'^api-auth/', include('rest_framework.urls', url(r'^api-auth/', include('rest_framework.urls',
namespace='rest_framework')), namespace='rest_framework')),
) ]
## Adding pagination ## Adding pagination
......
...@@ -60,7 +60,7 @@ To see what's going on under the hood let's first explicitly create a set of vie ...@@ -60,7 +60,7 @@ To see what's going on under the hood let's first explicitly create a set of vie
In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views. In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views.
from snippets.views import SnippetViewSet, UserViewSet from snippets.views import SnippetViewSet, UserViewSet, api_root
from rest_framework import renderers from rest_framework import renderers
snippet_list = SnippetViewSet.as_view({ snippet_list = SnippetViewSet.as_view({
...@@ -87,14 +87,14 @@ Notice how we're creating multiple views from each `ViewSet` class, by binding t ...@@ -87,14 +87,14 @@ Notice how we're creating multiple views from each `ViewSet` class, by binding t
Now that we've bound our resources into concrete views, we can register the views with the URL conf as usual. Now that we've bound our resources into concrete views, we can register the views with the URL conf as usual.
urlpatterns = format_suffix_patterns(patterns('snippets.views', urlpatterns = format_suffix_patterns([
url(r'^$', 'api_root'), url(r'^$', api_root),
url(r'^snippets/$', snippet_list, name='snippet-list'), url(r'^snippets/$', snippet_list, name='snippet-list'),
url(r'^snippets/(?P<pk>[0-9]+)/$', snippet_detail, name='snippet-detail'), url(r'^snippets/(?P<pk>[0-9]+)/$', snippet_detail, name='snippet-detail'),
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'), url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'),
url(r'^users/$', user_list, name='user-list'), url(r'^users/$', user_list, name='user-list'),
url(r'^users/(?P<pk>[0-9]+)/$', user_detail, name='user-detail') url(r'^users/(?P<pk>[0-9]+)/$', user_detail, name='user-detail')
)) ])
## Using Routers ## Using Routers
...@@ -102,7 +102,7 @@ Because we're using `ViewSet` classes rather than `View` classes, we actually do ...@@ -102,7 +102,7 @@ Because we're using `ViewSet` classes rather than `View` classes, we actually do
Here's our re-wired `urls.py` file. Here's our re-wired `urls.py` file.
from django.conf.urls import patterns, url, include from django.conf.urls import url, include
from snippets import views from snippets import views
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
...@@ -113,10 +113,10 @@ Here's our re-wired `urls.py` file. ...@@ -113,10 +113,10 @@ Here's our re-wired `urls.py` file.
# The API URLs are now determined automatically by the router. # The API URLs are now determined automatically by the router.
# Additionally, we include the login URLs for the browseable API. # Additionally, we include the login URLs for the browseable API.
urlpatterns = patterns('', urlpatterns = [
url(r'^', include(router.urls)), url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
) ]
Registering the viewsets with the router is similar to providing a urlpattern. We include two arguments - the URL prefix for the views, and the viewset itself. Registering the viewsets with the router is similar to providing a urlpattern. We include two arguments - the URL prefix for the views, and the viewset itself.
......
...@@ -19,18 +19,20 @@ Create a new Django project named `tutorial`, then start a new app called `quick ...@@ -19,18 +19,20 @@ Create a new Django project named `tutorial`, then start a new app called `quick
pip install djangorestframework pip install djangorestframework
# Set up a new project with a single application # Set up a new project with a single application
django-admin.py startproject tutorial . django-admin.py startproject tutorial
cd tutorial cd tutorial
django-admin.py startapp quickstart django-admin.py startapp quickstart
cd .. cd ..
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
......
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8"> <meta charset="utf-8">
<title>Django REST framework - 404 - Page not found</title> <title>Django REST framework - 404 - Page not found</title>
<link href="http://www.django-rest-framework.org/img/favicon.ico" rel="icon" type="image/x-icon"> <link href="http://www.django-rest-framework.org/img/favicon.ico" rel="icon" type="image/x-icon">
<link rel="canonical" href="http://www.django-rest-framework.org/404"/> <link rel="canonical" href="http://www.django-rest-framework.org/404" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Django, API, REST, 404 - Page not found"> <meta name="description" content="Django, API, REST, 404 - Page not found">
<meta name="author" content="Tom Christie"> <meta name="author" content="Tom Christie">
...@@ -21,20 +23,22 @@ ...@@ -21,20 +23,22 @@
<![endif]--> <![endif]-->
<script type="text/javascript"> <script type="text/javascript">
var _gaq = _gaq || []; var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-18852272-2']); _gaq.push(['_setAccount', 'UA-18852272-2']);
_gaq.push(['_trackPageview']); _gaq.push(['_trackPageview']);
(function() { (function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; var ga = document.createElement('script');
ga.type = 'text/javascript';
ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ga, s);
})(); })();
</script> </script>
</head> </head>
<body onload="prettyPrint()" class="404-page">
<body onload="prettyPrint()" class="404-page">
<div class="wrapper"> <div class="wrapper">
...@@ -121,16 +125,16 @@ ...@@ -121,16 +125,16 @@
</li> </li>
--> -->
</ul> </ul>
</div><!--/.nav-collapse --> </div>
<!--/.nav-collapse -->
</div> </div>
</div> </div>
</div> </div>
<div class="body-content"> <div class="body-content">
<div class="container-fluid"> <div class="container-fluid">
<!-- Search Modal -->
<!-- Search Modal --> <div id="searchModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div id="searchModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3 id="myModalLabel">Documentation search</h3> <h3 id="myModalLabel">Documentation search</h3>
...@@ -154,23 +158,30 @@ ...@@ -154,23 +158,30 @@
<div class="modal-footer"> <div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button> <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
</div> </div>
</div> </div>
<div class="row-fluid"> <div class="row-fluid">
<div id="main-content" class="span12"> <div id="main-content" class="span12">
<h1 id="404-page-not-found" style="text-align: center">404</h1> <h1 id="404-page-not-found" style="text-align: center">404</h1>
<p style="text-align: center"><strong>Page not found</strong></p> <p style="text-align: center"><strong>Page not found</strong>
</p>
<p style="text-align: center">Try the <a href="http://www.django-rest-framework.org/">homepage</a>, or <a href="#searchModal" data-toggle="modal">search the documentation</a>.</p> <p style="text-align: center">Try the <a href="http://www.django-rest-framework.org/">homepage</a>, or <a href="#searchModal" data-toggle="modal">search the documentation</a>.</p>
</div><!--/span--> </div>
</div><!--/row--> <!--/span-->
</div><!--/.fluid-container--> </div>
</div><!--/.body content--> <!--/row-->
</div>
<!--/.fluid-container-->
</div>
<!--/.body content-->
<div id="push"></div> <div id="push"></div>
</div><!--/.wrapper --> </div>
<!--/.wrapper -->
<footer class="span12"> <footer class="span12">
<p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</a></p> <p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</a>
</p>
</footer> </footer>
<!-- Le javascript <!-- Le javascript
...@@ -181,7 +192,9 @@ ...@@ -181,7 +192,9 @@
<script src="http://www.django-rest-framework.org/js/bootstrap-2.1.1-min.js"></script> <script src="http://www.django-rest-framework.org/js/bootstrap-2.1.1-min.js"></script>
<script> <script>
//$('.side-nav').scrollspy() //$('.side-nav').scrollspy()
var shiftWindow = function() { scrollBy(0, -50) }; var shiftWindow = function() {
scrollBy(0, -50)
};
if (location.hash) shiftWindow(); if (location.hash) shiftWindow();
window.addEventListener("hashchange", shiftWindow); window.addEventListener("hashchange", shiftWindow);
...@@ -192,10 +205,12 @@ ...@@ -192,10 +205,12 @@
// Dynamically force sidenav to no higher than browser window // Dynamically force sidenav to no higher than browser window
$('.side-nav').css('max-height', window.innerHeight - 130); $('.side-nav').css('max-height', window.innerHeight - 130);
$(function(){ $(function() {
$(window).resize(function(){ $(window).resize(function() {
$('.side-nav').css('max-height', window.innerHeight - 130); $('.side-nav').css('max-height', window.innerHeight - 130);
}); });
}); });
</script> </script>
</body></html> </body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<title>{{ page_title }}</title>
<link href="{{ base_url }}/img/favicon.ico" rel="icon" type="image/x-icon">
<link rel="canonical" href="{{ canonical_url }}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Django, API, REST, {{ current_page.title }}">
<meta name="author" content="Tom Christie">
<!-- Le styles -->
<link href="{{ base_url }}/css/prettify.css" rel="stylesheet">
<link href="{{ base_url }}/css/bootstrap.css" rel="stylesheet">
<link href="{{ base_url }}/css/bootstrap-responsive.css" rel="stylesheet">
<link href="{{ base_url }}/css/default.css" rel="stylesheet">
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-18852272-2']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script');
ga.type = 'text/javascript';
ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ga, s);
})();
</script>
<style>
span.fusion-wrap a {
display: block;
margin-top: 10px;
color: black;
}
a.fusion-poweredby {
display: block;
margin-top: 10px;
}
@media (max-width: 767px) {
div.promo {
display: none;
}
}
</style>
</head>
<body onload="prettyPrint()" class="{% if current_page.is_homepage %}index{% endif %}-page">
<div class="wrapper">
{% include "nav.html" %}
<div class="body-content">
<div class="container-fluid">
<!-- Search Modal -->
<div id="searchModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3 id="myModalLabel">Documentation search</h3>
</div>
<div class="modal-body">
<!-- Custom google search -->
<script>
(function() {
var cx = '015016005043623903336:rxraeohqk6w';
var gcse = document.createElement('script');
gcse.type = 'text/javascript';
gcse.async = true;
gcse.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') +
'//www.google.com/cse/cse.js?cx=' + cx;
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(gcse, s);
})();
</script>
<gcse:search></gcse:search>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
</div>
</div>
<div class="row-fluid">
<div class="span3">
<!-- TODO
<p style="margin-top: -12px">
<a class="btn btn-mini btn-primary" style="width: 60px">&laquo; previous</a>
<a class="btn btn-mini btn-primary" style="float: right; margin-right: 8px; width: 60px;">next &raquo;</a>
</p>
-->
<div id="table-of-contents">
<ul class="nav nav-list side-nav well sidebar-nav-fixed">
{% if current_page.is_homepage %}
<li class="main">
<a href="#">Django REST framework</a>
</li>
{% endif %}
{% for toc_item in toc %}
<li class="{% if not current_page.is_homepage %}main{% endif %}">
<a href="{{ toc_item.url }}">{{ toc_item.title }}</a>
</li>
{% for toc_item in toc_item.children %}
<li>
<a href="{{ toc_item.url }}">{{ toc_item.title }}</a>
</li>
{% endfor %}
{% endfor %}
{% if current_page.is_homepage %}
<div class="promo">
<hr/>
<script type="text/javascript" src="//cdn.fusionads.net/fusion.js?zoneid=1332&serve=C6SDP2Y&placement=djangorestframework" id="_fusionads_js"></script>
</div>
{% endif %}
</ul>
</div>
</div>
<div id="main-content" class="span9">
{% if meta.source %}
{% for filename in meta.source %}
<a class="github" href="https://github.com/tomchristie/django-rest-framework/tree/master/rest_framework/{{ filename }}">
<span class="label label-info">{{ filename }}</span>
</a>
{% endfor %}
{% endif %}
{{ content }}
</div>
<!--/span-->
</div>
<!--/row-->
</div>
<!--/.fluid-container-->
</div>
<!--/.body content-->
<div id="push"></div>
</div>
<!--/.wrapper -->
<footer class="span12">
<p>Documentation built with <a href="http://www.mkdocs.org/">MkDocs</a>.</a>
</p>
</footer>
<!-- Le javascript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="{{ base_url }}/js/jquery-1.8.1-min.js"></script>
<script src="{{ base_url }}/js/prettify-1.0.js"></script>
<script src="{{ base_url }}/js/bootstrap-2.1.1-min.js"></script>
<script>
//$('.side-nav').scrollspy()
var shiftWindow = function() {
scrollBy(0, -50)
};
if (location.hash) shiftWindow();
window.addEventListener("hashchange", shiftWindow);
$('.dropdown-menu').on('click touchstart', function(event) {
event.stopPropagation();
});
// Dynamically force sidenav to no higher than browser window
$('.side-nav').css('max-height', window.innerHeight - 130);
$(function() {
$(window).resize(function() {
$('.side-nav').css('max-height', window.innerHeight - 130);
});
});
</script>
</body>
</html>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container-fluid">
<a class="repo-link btn btn-primary btn-small" href="https://github.com/tomchristie/django-rest-framework/tree/master">GitHub</a>
<a class="repo-link btn btn-inverse btn-small {% if not next_page %}disabled{% endif %}" rel="prev" {% if next_page %}href="{{ next_page.url }}"{% endif %}>
Next <i class="icon-arrow-right icon-white"></i>
</a>
<a class="repo-link btn btn-inverse btn-small {% if not previous_page %}disabled{% endif %}" rel="next" {% if previous_page %}href="{{ previous_page.url }}"{% endif %}>
<i class="icon-arrow-left icon-white"></i> Previous
</a>
<a class="repo-link btn btn-inverse btn-small" href="#searchModal" data-toggle="modal"><i class="icon-search icon-white"></i> Search</a>
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="http://www.django-rest-framework.org">Django REST framework</a>
<div class="nav-collapse collapse">
{% if include_nav %}
<!-- Main navigation -->
<ul class="nav navbar-nav">
<li {% if current_page.is_homepage %}class="active"{% endif %}><a href="/">Home</a></li>
{% for nav_item in nav %} {% if nav_item.children %}
<li class="dropdown{% if nav_item.active %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ nav_item.title }} <b class="caret"></b></a>
<ul class="dropdown-menu">
{% for nav_item in nav_item.children %}
<li {% if nav_item.active %}class="active" {% endif %}>
<a href="{{ nav_item.url }}">{{ nav_item.title }}</a>
</li>
{% endfor %}
</ul>
</li>
{% else %}
<li {% if nav_item.active %}class="active" {% endif %}>
<a href="{{ nav_item.url }}">{{ nav_item.title }}</a>
</li>
{% endif %} {% endfor %}
</ul>
{% endif %}
</div>
<!--/.nav-collapse -->
</div>
</div>
</div>
#!/usr/bin/env python
import markdown
import os
import re
import shutil
import sys
root_dir = os.path.abspath(os.path.dirname(__file__))
docs_dir = os.path.join(root_dir, 'docs')
html_dir = os.path.join(root_dir, 'html')
local = not '--deploy' in sys.argv
preview = '-p' in sys.argv
if local:
base_url = 'file://%s/' % os.path.normpath(os.path.join(os.getcwd(), html_dir))
suffix = '.html'
index = 'index.html'
else:
base_url = 'http://www.django-rest-framework.org'
suffix = ''
index = ''
main_header = '<li class="main"><a href="#{{ anchor }}">{{ title }}</a></li>'
sub_header = '<li><a href="#{{ anchor }}">{{ title }}</a></li>'
code_label = r'<a class="github" href="https://github.com/tomchristie/django-rest-framework/tree/master/rest_framework/\1"><span class="label label-info">\1</span></a>'
page = open(os.path.join(docs_dir, 'template.html'), 'r').read()
# Copy static files
# for static in ['css', 'js', 'img']:
# source = os.path.join(docs_dir, 'static', static)
# target = os.path.join(html_dir, static)
# if os.path.exists(target):
# shutil.rmtree(target)
# shutil.copytree(source, target)
# Hacky, but what the hell, it'll do the job
path_list = [
'index.md',
'tutorial/quickstart.md',
'tutorial/1-serialization.md',
'tutorial/2-requests-and-responses.md',
'tutorial/3-class-based-views.md',
'tutorial/4-authentication-and-permissions.md',
'tutorial/5-relationships-and-hyperlinked-apis.md',
'tutorial/6-viewsets-and-routers.md',
'api-guide/requests.md',
'api-guide/responses.md',
'api-guide/views.md',
'api-guide/generic-views.md',
'api-guide/viewsets.md',
'api-guide/routers.md',
'api-guide/parsers.md',
'api-guide/renderers.md',
'api-guide/serializers.md',
'api-guide/fields.md',
'api-guide/relations.md',
'api-guide/authentication.md',
'api-guide/permissions.md',
'api-guide/throttling.md',
'api-guide/filtering.md',
'api-guide/pagination.md',
'api-guide/content-negotiation.md',
'api-guide/format-suffixes.md',
'api-guide/reverse.md',
'api-guide/exceptions.md',
'api-guide/status-codes.md',
'api-guide/testing.md',
'api-guide/settings.md',
'topics/documenting-your-api.md',
'topics/ajax-csrf-cors.md',
'topics/browser-enhancements.md',
'topics/browsable-api.md',
'topics/rest-hypermedia-hateoas.md',
'topics/third-party-resources.md',
'topics/contributing.md',
'topics/rest-framework-2-announcement.md',
'topics/2.2-announcement.md',
'topics/2.3-announcement.md',
'topics/2.4-announcement.md',
'topics/release-notes.md',
'topics/credits.md',
]
prev_url_map = {}
next_url_map = {}
for idx in range(len(path_list)):
path = path_list[idx]
rel = '../' * path.count('/')
if idx == 1 and not local:
# Link back to '/', not '/index'
prev_url_map[path] = '/'
elif idx > 0:
prev_url_map[path] = rel + path_list[idx - 1][:-3] + suffix
if idx < len(path_list) - 1:
next_url_map[path] = rel + path_list[idx + 1][:-3] + suffix
for (dirpath, dirnames, filenames) in os.walk(docs_dir):
relative_dir = dirpath.replace(docs_dir, '').lstrip(os.path.sep)
build_dir = os.path.join(html_dir, relative_dir)
if not os.path.exists(build_dir):
os.makedirs(build_dir)
for filename in filenames:
path = os.path.join(dirpath, filename)
relative_path = os.path.join(relative_dir, filename)
if not filename.endswith('.md'):
if relative_dir:
output_path = os.path.join(build_dir, filename)
shutil.copy(path, output_path)
continue
output_path = os.path.join(build_dir, filename[:-3] + '.html')
toc = ''
text = open(path, 'r').read().decode('utf-8')
main_title = None
description = 'Django, API, REST'
for line in text.splitlines():
if line.startswith('# '):
title = line[2:].strip()
template = main_header
description = description + ', ' + title
elif line.startswith('## '):
title = line[3:].strip()
template = sub_header
else:
continue
if not main_title:
main_title = title
anchor = title.lower().replace(' ', '-').replace(':-', '-').replace("'", '').replace('?', '').replace('.', '')
template = template.replace('{{ title }}', title)
template = template.replace('{{ anchor }}', anchor)
toc += template + '\n'
if filename == 'index.md':
main_title = 'Django REST framework - Web APIs for Django'
else:
main_title = main_title + ' - Django REST framework'
if relative_path == 'index.md':
canonical_url = base_url
else:
canonical_url = base_url + '/' + relative_path[:-3] + suffix
prev_url = prev_url_map.get(relative_path)
next_url = next_url_map.get(relative_path)
content = markdown.markdown(text, ['headerid'])
output = page.replace('{{ content }}', content).replace('{{ toc }}', toc).replace('{{ base_url }}', base_url).replace('{{ suffix }}', suffix).replace('{{ index }}', index)
output = output.replace('{{ title }}', main_title)
output = output.replace('{{ description }}', description)
output = output.replace('{{ page_id }}', filename[:-3])
output = output.replace('{{ canonical_url }}', canonical_url)
if filename =='index.md':
output = output.replace('{{ ad_block }}', """<hr/>
<script type="text/javascript" src="//cdn.fusionads.net/fusion.js?zoneid=1332&serve=C6SDP2Y&placement=djangorestframework" id="_fusionads_js"></script>""")
else:
output = output.replace('{{ ad_block }}', '')
if prev_url:
output = output.replace('{{ prev_url }}', prev_url)
output = output.replace('{{ prev_url_disabled }}', '')
else:
output = output.replace('{{ prev_url }}', '#')
output = output.replace('{{ prev_url_disabled }}', 'disabled')
if next_url:
output = output.replace('{{ next_url }}', next_url)
output = output.replace('{{ next_url_disabled }}', '')
else:
output = output.replace('{{ next_url }}', '#')
output = output.replace('{{ next_url_disabled }}', 'disabled')
output = re.sub(r'a href="([^"]*)\.md"', r'a href="\1%s"' % suffix, output)
output = re.sub(r'<pre><code>:::bash', r'<pre class="prettyprint lang-bsh">', output)
output = re.sub(r'<pre>', r'<pre class="prettyprint lang-py">', output)
output = re.sub(r'<a class="github" href="([^"]*)"></a>', code_label, output)
open(output_path, 'w').write(output.encode('utf-8'))
if preview:
import subprocess
url = 'html/index.html'
try:
subprocess.Popen(["open", url]) # Mac
except OSError:
subprocess.Popen(["xdg-open", url]) # Linux
except:
os.startfile(url) # Windows
site_name: Django REST framework
site_url: http://www.django-rest-framework.org/
site_description: Django REST framework - Web APIs for Django
repo_url: https://github.com/tomchristie/django-rest-framework
theme_dir: docs_theme
pages:
- ['index.md', 'Home']
- ['tutorial/quickstart.md', 'Tutorial', 'Quickstart']
- ['tutorial/1-serialization.md', 'Tutorial', '1 - Serialization']
- ['tutorial/2-requests-and-responses.md', 'Tutorial', '2 - Requests and responses']
- ['tutorial/3-class-based-views.md', 'Tutorial', '3 - Class based views']
- ['tutorial/4-authentication-and-permissions.md', 'Tutorial', '4 - Authentication and permissions']
- ['tutorial/5-relationships-and-hyperlinked-apis.md', 'Tutorial', '5 - Relationships and hyperlinked APIs']
- ['tutorial/6-viewsets-and-routers.md', 'Tutorial', '6 - Viewsets and routers']
- ['api-guide/requests.md', 'API Guide', 'Requests']
- ['api-guide/responses.md', 'API Guide', 'Responses']
- ['api-guide/views.md', 'API Guide', 'Views']
- ['api-guide/generic-views.md', 'API Guide', 'Generic views']
- ['api-guide/viewsets.md', 'API Guide', 'Viewsets']
- ['api-guide/routers.md', 'API Guide', 'Routers']
- ['api-guide/parsers.md', 'API Guide', 'Parsers']
- ['api-guide/renderers.md', 'API Guide', 'Renderers']
- ['api-guide/serializers.md', 'API Guide', 'Serializers']
- ['api-guide/fields.md', 'API Guide', 'Serializer fields']
- ['api-guide/relations.md', 'API Guide', 'Serializer relations']
- ['api-guide/validators.md', 'API Guide', 'Validators']
- ['api-guide/authentication.md', 'API Guide', 'Authentication']
- ['api-guide/permissions.md', 'API Guide', 'Permissions']
- ['api-guide/throttling.md', 'API Guide', 'Throttling']
- ['api-guide/filtering.md', 'API Guide', 'Filtering']
- ['api-guide/pagination.md', 'API Guide', 'Pagination']
- ['api-guide/content-negotiation.md', 'API Guide', 'Content negotiation']
- ['api-guide/format-suffixes.md', 'API Guide', 'Format suffixes']
- ['api-guide/reverse.md', 'API Guide', 'Returning URLs']
- ['api-guide/exceptions.md', 'API Guide', 'Exceptions']
- ['api-guide/status-codes.md', 'API Guide', 'Status codes']
- ['api-guide/testing.md', 'API Guide', 'Testing']
- ['api-guide/settings.md', 'API Guide', 'Settings']
- ['topics/documenting-your-api.md', 'Topics', 'Documenting your API']
- ['topics/ajax-csrf-cors.md', 'Topics', 'AJAX, CSRF & CORS']
- ['topics/browser-enhancements.md', 'Topics',]
- ['topics/browsable-api.md', 'Topics', 'The Browsable API']
- ['topics/rest-hypermedia-hateoas.md', 'Topics', 'REST, Hypermedia & HATEOAS']
- ['topics/third-party-resources.md', 'Topics', 'Third Party Resources']
- ['topics/contributing.md', 'Topics', 'Contributing to REST framework']
- ['topics/rest-framework-2-announcement.md', 'Topics', '2.0 Announcement']
- ['topics/2.2-announcement.md', 'Topics', '2.2 Announcement']
- ['topics/2.3-announcement.md', 'Topics', '2.3 Announcement']
- ['topics/2.4-announcement.md', 'Topics', '2.4 Announcement']
- ['topics/kickstarter-announcement.md', 'Topics', 'Kickstarter Announcement']
- ['topics/release-notes.md', 'Topics', 'Release Notes']
- ['topics/credits.md', 'Topics', 'Credits']
...@@ -8,7 +8,7 @@ ______ _____ _____ _____ __ ...@@ -8,7 +8,7 @@ ______ _____ _____ _____ __
""" """
__title__ = 'Django REST framework' __title__ = 'Django REST framework'
__version__ = '2.4.2' __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'
......
...@@ -124,7 +124,7 @@ class SessionAuthentication(BaseAuthentication): ...@@ -124,7 +124,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,13 +18,13 @@ class AuthTokenSerializer(serializers.Serializer): ...@@ -18,13 +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)
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 attrs['user'] = user
return attrs return attrs
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework import status
from rest_framework import parsers from rest_framework import parsers
from rest_framework import renderers from rest_framework import renderers
from rest_framework.response import Response from rest_framework.response import Response
...@@ -12,16 +11,13 @@ class ObtainAuthToken(APIView): ...@@ -12,16 +11,13 @@ class ObtainAuthToken(APIView):
permission_classes = () permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,) renderer_classes = (renderers.JSONRenderer,)
serializer_class = AuthTokenSerializer
model = Token
def post(self, request): def post(self, request):
serializer = self.serializer_class(data=request.DATA) serializer = AuthTokenSerializer(data=request.data)
if serializer.is_valid(): serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user'] user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user) token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key}) return Response({'token': token.key})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
obtain_auth_token = ObtainAuthToken.as_view() obtain_auth_token = ObtainAuthToken.as_view()
...@@ -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.
...@@ -25,6 +26,16 @@ except ImportError: ...@@ -25,6 +26,16 @@ except ImportError:
from django.utils.encoding import force_unicode as force_text from django.utils.encoding import force_unicode as force_text
# OrderedDict only available in Python 2.7.
# This will always be the case in Django 1.7 and above, as these versions
# no longer support Python 2.6.
# For Django <= 1.6 and Python 2.6 fall back to OrderedDict.
try:
from collections import OrderedDict
except:
from django.utils.datastructures import SortedDict as OrderedDict
# HttpResponseBase only exists from 1.5 onwards # HttpResponseBase only exists from 1.5 onwards
try: try:
from django.http.response import HttpResponseBase from django.http.response import HttpResponseBase
...@@ -84,15 +95,6 @@ except ImportError: ...@@ -84,15 +95,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:
...@@ -121,6 +123,62 @@ else: ...@@ -121,6 +123,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']
...@@ -144,12 +202,12 @@ class RequestFactory(DjangoRequestFactory): ...@@ -144,12 +202,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):
...@@ -193,6 +251,16 @@ except ImportError: ...@@ -193,6 +251,16 @@ except ImportError:
etree = None etree = 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
......
...@@ -5,80 +5,143 @@ In addition Django's built in 403 and 404 exceptions are handled. ...@@ -5,80 +5,143 @@ In addition Django's built in 403 and 404 exceptions are handled.
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`) (`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from rest_framework import status from rest_framework import status
from rest_framework.compat import force_text
import math import math
def _force_text_recursive(data):
"""
Descend into a nested data structure, forcing any
lazy translation strings into plain text.
"""
if isinstance(data, list):
return [
_force_text_recursive(item) for item in data
]
elif isinstance(data, dict):
return dict([
(key, _force_text_recursive(value))
for key, value in data.items()
])
return force_text(data)
class APIException(Exception): class APIException(Exception):
""" """
Base class for REST framework exceptions. Base class for REST framework exceptions.
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 = 'A server error occured' default_detail = _('A server error occured')
def __init__(self, detail=None): def __init__(self, detail=None):
self.detail = detail or self.default_detail if detail is not None:
self.detail = force_text(detail)
else:
self.detail = force_text(self.default_detail)
def __str__(self): def __str__(self):
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 = _force_text_recursive(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.')
class AuthenticationFailed(APIException): class AuthenticationFailed(APIException):
status_code = status.HTTP_401_UNAUTHORIZED status_code = status.HTTP_401_UNAUTHORIZED
default_detail = 'Incorrect authentication credentials.' default_detail = _('Incorrect authentication credentials.')
class NotAuthenticated(APIException): class NotAuthenticated(APIException):
status_code = status.HTTP_401_UNAUTHORIZED status_code = status.HTTP_401_UNAUTHORIZED
default_detail = 'Authentication credentials were not provided.' default_detail = _('Authentication credentials were not provided.')
class PermissionDenied(APIException): class PermissionDenied(APIException):
status_code = status.HTTP_403_FORBIDDEN status_code = status.HTTP_403_FORBIDDEN
default_detail = 'You do not have permission to perform this action.' default_detail = _('You do not have permission to perform this action.')
class MethodNotAllowed(APIException): class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED status_code = status.HTTP_405_METHOD_NOT_ALLOWED
default_detail = "Method '%s' not allowed." default_detail = _("Method '%s' not allowed.")
def __init__(self, method, detail=None): def __init__(self, method, detail=None):
self.detail = (detail or self.default_detail) % method if detail is not None:
self.detail = force_text(detail)
else:
self.detail = force_text(self.default_detail) % method
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 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 if detail is not None:
self.detail = force_text(detail)
else:
self.detail = force_text(self.default_detail)
self.available_renderers = available_renderers self.available_renderers = available_renderers
class UnsupportedMediaType(APIException): class UnsupportedMediaType(APIException):
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
default_detail = "Unsupported media type '%s' in request." default_detail = _("Unsupported media type '%s' in request.")
def __init__(self, media_type, detail=None): def __init__(self, media_type, detail=None):
self.detail = (detail or self.default_detail) % media_type if detail is not None:
self.detail = force_text(detail)
else:
self.detail = force_text(self.default_detail) % media_type
class Throttled(APIException): class Throttled(APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS status_code = status.HTTP_429_TOO_MANY_REQUESTS
default_detail = 'Request was throttled.' default_detail = _('Request was throttled.')
extra_detail = " Expected available in %d second%s." extra_detail = ungettext_lazy(
'Expected available in %(wait)d second.',
'Expected available in %(wait)d seconds.',
'wait'
)
def __init__(self, wait=None, detail=None): def __init__(self, wait=None, detail=None):
if detail is not None:
self.detail = force_text(detail)
else:
self.detail = force_text(self.default_detail)
if wait is None: if wait is None:
self.detail = detail or self.default_detail
self.wait = None self.wait = None
else: else:
format = (detail or self.default_detail) + self.extra_detail
self.detail = format % (wait, wait != 1 and 's' or '')
self.wait = math.ceil(wait) self.wait = math.ceil(wait)
self.detail += ' ' + force_text(
self.extra_detail % {'wait': self.wait}
)
...@@ -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()
......
...@@ -3,14 +3,13 @@ Generic views that provide commonly needed behaviour. ...@@ -3,14 +3,13 @@ Generic views that provide commonly needed behaviour.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db.models.query import QuerySet
from django.core.exceptions import PermissionDenied
from django.core.paginator import Paginator, InvalidPage from django.core.paginator import Paginator, InvalidPage
from django.db.models.query import QuerySet
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404 as _get_object_or_404 from django.shortcuts import get_object_or_404 as _get_object_or_404
from django.utils import six
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from rest_framework import views, mixins, exceptions from rest_framework import views, mixins
from rest_framework.request import clone_request
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
...@@ -114,7 +113,7 @@ class GenericAPIView(views.APIView): ...@@ -114,7 +113,7 @@ class GenericAPIView(views.APIView):
paginator = self.paginator_class(queryset, page_size) paginator = self.paginator_class(queryset, page_size)
page_kwarg = self.kwargs.get(self.page_kwarg) page_kwarg = self.kwargs.get(self.page_kwarg)
page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg) page_query_param = self.request.query_params.get(self.page_kwarg)
page = page_kwarg or page_query_param or 1 page = page_kwarg or page_query_param or 1
try: try:
page_number = paginator.validate_number(page) page_number = paginator.validate_number(page)
...@@ -129,7 +128,7 @@ class GenericAPIView(views.APIView): ...@@ -129,7 +128,7 @@ class GenericAPIView(views.APIView):
error_format = _('Invalid page (%(page_number)s): %(message)s') error_format = _('Invalid page (%(page_number)s): %(message)s')
raise Http404(error_format % { raise Http404(error_format % {
'page_number': page_number, 'page_number': page_number,
'message': str(exc) 'message': six.text_type(exc)
}) })
return page return page
...@@ -168,7 +167,7 @@ class GenericAPIView(views.APIView): ...@@ -168,7 +167,7 @@ class GenericAPIView(views.APIView):
if self.paginate_by_param: if self.paginate_by_param:
try: try:
return strict_positive_int( return strict_positive_int(
self.request.QUERY_PARAMS[self.paginate_by_param], self.request.query_params[self.paginate_by_param],
cutoff=self.max_paginate_by cutoff=self.max_paginate_by
) )
except (KeyError, ValueError): except (KeyError, ValueError):
...@@ -249,53 +248,6 @@ class GenericAPIView(views.APIView): ...@@ -249,53 +248,6 @@ class GenericAPIView(views.APIView):
return obj return obj
# The following are placeholder methods,
# and are intended to be overridden.
#
# The are not called by GenericAPIView directly,
# but are used by the mixin methods.
def metadata(self, request):
"""
Return a dictionary of metadata about the view.
Used to return responses for OPTIONS requests.
We override the default behavior, and add some extra information
about the required request body for POST and PUT operations.
"""
ret = super(GenericAPIView, self).metadata(request)
actions = {}
for method in ('PUT', 'POST'):
if method not in self.allowed_methods:
continue
cloned_request = clone_request(request, method)
try:
# Test global permissions
self.check_permissions(cloned_request)
# Test object permissions
if method == 'PUT':
try:
self.get_object()
except Http404:
# Http404 should be acceptable and the serializer
# metadata should be populated. Except this so the
# outer "else" clause of the try-except-else block
# will be executed.
pass
except (exceptions.APIException, PermissionDenied):
pass
else:
# If user has appropriate permissions for the view, include
# appropriate metadata about the fields that should be supplied.
serializer = self.get_serializer()
actions[method] = serializer.metadata()
if actions:
ret['actions'] = actions
return ret
# Concrete view classes that provide method handlers # Concrete view classes that provide method handlers
# by composing the mixin classes with the base view. # by composing the mixin classes with the base view.
......
"""
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 rest_framework import exceptions, serializers
from rest_framework.compat import force_text, OrderedDict
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 = OrderedDict()
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.
"""
if hasattr(serializer, 'child'):
# If this is a `ListSerializer` then we want to examine the
# underlying child serializer instance instead.
serializer = serializer.child
return OrderedDict([
(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 = OrderedDict()
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': force_text(choice_name, strings_only=True)
}
for choice_value, choice_name in field.choices.items()
]
return field_info
...@@ -6,10 +6,8 @@ which allows mixin classes to be composed in interesting ways. ...@@ -6,10 +6,8 @@ which allows mixin classes to be composed in interesting ways.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
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
...@@ -18,12 +16,15 @@ class CreateModelMixin(object): ...@@ -18,12 +16,15 @@ class CreateModelMixin(object):
Create a model instance. Create a model instance.
""" """
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() self.perform_create(serializer)
headers = self.get_success_headers(serializer.data) headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
serializer.save()
def get_success_headers(self, data): def get_success_headers(self, data):
try: try:
return {'Location': data[api_settings.URL_FIELD_NAME]} return {'Location': data[api_settings.URL_FIELD_NAME]}
...@@ -62,11 +63,14 @@ class UpdateModelMixin(object): ...@@ -62,11 +63,14 @@ class UpdateModelMixin(object):
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False) partial = kwargs.pop('partial', False)
instance = self.get_object() instance = self.get_object()
serializer = self.get_serializer(instance, data=request.DATA, partial=partial) serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() self.perform_update(serializer)
return Response(serializer.data) return Response(serializer.data)
def perform_update(self, serializer):
serializer.save()
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)
...@@ -78,51 +82,8 @@ class DestroyModelMixin(object): ...@@ -78,51 +82,8 @@ class DestroyModelMixin(object):
""" """
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
instance.delete() self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
def perform_destroy(self, instance):
# The AllowPUTAsCreateMixin was previously the default behaviour instance.delete()
# for PUT requests. This has now been removed and must be *explictly*
# included if it is the behavior that you want.
# For more info see: ...
class AllowPUTAsCreateMixin(object):
"""
The following mixin class may be used in order to support PUT-as-create
behavior for incoming requests.
"""
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object_or_none()
serializer = self.get_serializer(instance, data=request.DATA, partial=partial)
serializer.is_valid(raise_exception=True)
if instance is None:
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
lookup_value = self.kwargs[lookup_url_kwarg]
extras = {self.lookup_field: lookup_value}
serializer.save(extras=extras)
return Response(serializer.data, status=status.HTTP_201_CREATED)
serializer.save()
return Response(serializer.data)
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
def get_object_or_none(self):
try:
return self.get_object()
except Http404:
if self.request.method == 'PUT':
# For PUT-as-create operation, we need to ensure that we have
# relevant permissions, as if this was a POST request. This
# will either raise a PermissionDenied exception, or simply
# return None.
self.check_permissions(clone_request(self.request, 'POST'))
else:
# PATCH requests where the object does not exist should still
# return a 404 response.
raise
...@@ -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(',')]
...@@ -72,7 +72,6 @@ class BasePaginationSerializer(serializers.Serializer): ...@@ -72,7 +72,6 @@ class BasePaginationSerializer(serializers.Serializer):
child=object_serializer(), child=object_serializer(),
source='object_list' source='object_list'
) )
self.fields[results_field].bind(results_field, self, self)
class PaginationSerializer(BasePaginationSerializer): class PaginationSerializer(BasePaginationSerializer):
......
...@@ -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
...@@ -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):
......
from rest_framework.compat import smart_text, urlparse from rest_framework.compat import smart_text, urlparse
from rest_framework.fields import Field from rest_framework.fields import get_attribute, empty, Field
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from rest_framework.utils import html
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch, Resolver404 from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch, Resolver404
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.utils import six
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
class PKOnlyObject(object):
"""
This is a mock object, used for when we only need the pk of the object
instance, but still want to return an object with a .pk attribute,
in order to keep the same interface as a regular model instance.
"""
def __init__(self, pk):
self.pk = pk
# We assume that 'validators' are intended for the child serializer,
# rather than the parent serializer.
MANY_RELATION_KWARGS = (
'read_only', 'write_only', 'required', 'default', 'initial', 'source',
'label', 'help_text', 'style', 'error_messages'
)
class RelatedField(Field): class RelatedField(Field):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.queryset = kwargs.pop('queryset', None) self.queryset = kwargs.pop('queryset', None)
...@@ -22,14 +42,40 @@ class RelatedField(Field): ...@@ -22,14 +42,40 @@ class RelatedField(Field):
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
# We override this method in order to automagically create # We override this method in order to automagically create
# `ManyRelation` classes instead when `many=True` is set. # `ManyRelatedField` classes instead when `many=True` is set.
if kwargs.pop('many', False): if kwargs.pop('many', False):
return ManyRelation( return cls.many_init(*args, **kwargs)
child_relation=cls(*args, **kwargs),
read_only=kwargs.get('read_only', False)
)
return super(RelatedField, cls).__new__(cls, *args, **kwargs) return super(RelatedField, cls).__new__(cls, *args, **kwargs)
@classmethod
def many_init(cls, *args, **kwargs):
"""
This method handles creating a parent `ManyRelatedField` instance
when the `many=True` keyword argument is passed.
Typically you won't need to override this method.
Note that we're over-cautious in passing most arguments to both parent
and child classes in order to try to cover the general case. If you're
overriding this method you'll probably want something much simpler, eg:
@classmethod
def many_init(cls, *args, **kwargs):
kwargs['child'] = cls()
return CustomManyRelatedField(*args, **kwargs)
"""
list_kwargs = {'child_relation': cls(*args, **kwargs)}
for key in kwargs.keys():
if key in MANY_RELATION_KWARGS:
list_kwargs[key] = kwargs[key]
return ManyRelatedField(**list_kwargs)
def run_validation(self, data=empty):
# We force empty strings to None values for relational fields.
if data == '':
data = None
return super(RelatedField, self).run_validation(data)
def get_queryset(self): def get_queryset(self):
queryset = self.queryset queryset = self.queryset
if isinstance(queryset, QuerySet): if isinstance(queryset, QuerySet):
...@@ -37,8 +83,22 @@ class RelatedField(Field): ...@@ -37,8 +83,22 @@ class RelatedField(Field):
queryset = queryset.all() queryset = queryset.all()
return queryset return queryset
def get_iterable(self, instance, source_attrs):
relationship = get_attribute(instance, source_attrs)
return relationship.all() if (hasattr(relationship, 'all')) else relationship
@property
def choices(self):
return dict([
(
str(self.to_representation(item)),
str(item)
)
for item in self.queryset.all()
])
class StringRelatedField(Field): class StringRelatedField(RelatedField):
""" """
A read only field that represents its targets using their A read only field that represents its targets using their
plain string representation. plain string representation.
...@@ -49,7 +109,7 @@ class StringRelatedField(Field): ...@@ -49,7 +109,7 @@ class StringRelatedField(Field):
super(StringRelatedField, self).__init__(**kwargs) super(StringRelatedField, self).__init__(**kwargs)
def to_representation(self, value): def to_representation(self, value):
return str(value) return six.text_type(value)
class PrimaryKeyRelatedField(RelatedField): class PrimaryKeyRelatedField(RelatedField):
...@@ -67,6 +127,32 @@ class PrimaryKeyRelatedField(RelatedField): ...@@ -67,6 +127,32 @@ class PrimaryKeyRelatedField(RelatedField):
except (TypeError, ValueError): except (TypeError, ValueError):
self.fail('incorrect_type', data_type=type(data).__name__) self.fail('incorrect_type', data_type=type(data).__name__)
def get_attribute(self, instance):
# We customize `get_attribute` here for performance reasons.
# For relationships the instance will already have the pk of
# the related object. We return this directly instead of returning the
# object itself, which would require a database lookup.
try:
instance = get_attribute(instance, self.source_attrs[:-1])
return PKOnlyObject(pk=instance.serializable_value(self.source_attrs[-1]))
except AttributeError:
return get_attribute(instance, self.source_attrs)
def get_iterable(self, instance, source_attrs):
# For consistency with `get_attribute` we're using `serializable_value()`
# here. Typically there won't be any difference, but some custom field
# types might return a non-primative value for the pk otherwise.
#
# We could try to get smart with `values_list('pk', flat=True)`, which
# would be better in some case, but would actually end up with *more*
# queries if the developer is using `prefetch_related` across the
# relationship.
relationship = super(PrimaryKeyRelatedField, self).get_iterable(instance, source_attrs)
return [
PKOnlyObject(pk=item.serializable_value('pk'))
for item in relationship
]
def to_representation(self, value): def to_representation(self, value):
return value.pk return value.pk
...@@ -89,9 +175,9 @@ class HyperlinkedRelatedField(RelatedField): ...@@ -89,9 +175,9 @@ class HyperlinkedRelatedField(RelatedField):
self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field) self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
self.format = kwargs.pop('format', None) self.format = kwargs.pop('format', None)
# We include these simply for dependancy injection in tests. # We include these simply for dependency injection in tests.
# We can't add them as class attributes or they would expect an # We can't add them as class attributes or they would expect an
# implict `self` argument to be passed. # implicit `self` argument to be passed.
self.reverse = reverse self.reverse = reverse
self.resolve = resolve self.resolve = resolve
...@@ -227,26 +313,33 @@ class SlugRelatedField(RelatedField): ...@@ -227,26 +313,33 @@ class SlugRelatedField(RelatedField):
return getattr(obj, self.slug_field) return getattr(obj, self.slug_field)
class ManyRelation(Field): class ManyRelatedField(Field):
""" """
Relationships with `many=True` transparently get coerced into instead being Relationships with `many=True` transparently get coerced into instead being
a ManyRelation with a child relationship. a ManyRelatedField with a child relationship.
The `ManyRelation` class is responsible for handling iterating through The `ManyRelatedField` class is responsible for handling iterating through
the values and passing each one to the child relationship. the values and passing each one to the child relationship.
You shouldn't need to be using this class directly yourself. This class is treated as private API.
You shouldn't generally need to be using this class directly yourself,
and should instead simply set 'many=True' on the relationship.
""" """
initial = []
default_empty_html = []
def __init__(self, child_relation=None, *args, **kwargs): def __init__(self, child_relation=None, *args, **kwargs):
self.child_relation = child_relation self.child_relation = child_relation
assert child_relation is not None, '`child_relation` is a required argument.' assert child_relation is not None, '`child_relation` is a required argument.'
super(ManyRelation, self).__init__(*args, **kwargs) super(ManyRelatedField, self).__init__(*args, **kwargs)
self.child_relation.bind(field_name='', parent=self)
def bind(self, field_name, parent, root): def get_value(self, dictionary):
# ManyRelation needs to provide the current context to the child relation. # We override the default field access in order to support
super(ManyRelation, self).bind(field_name, parent, root) # lists in HTML forms.
self.child_relation.bind(field_name, parent, root) if html.is_html_input(dictionary):
return dictionary.getlist(self.field_name)
return dictionary.get(self.field_name, empty)
def to_internal_value(self, data): def to_internal_value(self, data):
return [ return [
...@@ -254,8 +347,27 @@ class ManyRelation(Field): ...@@ -254,8 +347,27 @@ class ManyRelation(Field):
for item in data for item in data
] ]
def to_representation(self, obj): def get_attribute(self, instance):
return self.child_relation.get_iterable(instance, self.source_attrs)
def to_representation(self, iterable):
return [ return [
self.child_relation.to_representation(value) self.child_relation.to_representation(value)
for value in obj.all() for value in iterable
] ]
@property
def choices(self):
queryset = self.child_relation.queryset
iterable = queryset.all() if (hasattr(queryset, 'all')) else queryset
items_and_representations = [
(item, self.child_relation.to_representation(item))
for item in iterable
]
return dict([
(
str(item_representation),
str(item) + ' - ' + str(item_representation)
)
for item, item_representation in items_and_representations
])
...@@ -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
...@@ -91,6 +103,10 @@ def clone_request(request, method): ...@@ -91,6 +103,10 @@ def clone_request(request, method):
ret._auth = request._auth ret._auth = request._auth
if hasattr(request, '_authenticator'): if hasattr(request, '_authenticator'):
ret._authenticator = request._authenticator ret._authenticator = request._authenticator
if hasattr(request, 'accepted_renderer'):
ret.accepted_renderer = request.accepted_renderer
if hasattr(request, 'accepted_media_type'):
ret.accepted_media_type = request.accepted_media_type
return ret return ret
...@@ -133,6 +149,7 @@ class Request(object): ...@@ -133,6 +149,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 +203,31 @@ class Request(object): ...@@ -186,13 +203,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 +235,11 @@ class Request(object): ...@@ -200,6 +235,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 +252,11 @@ class Request(object): ...@@ -212,6 +252,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
...@@ -265,13 +310,17 @@ class Request(object): ...@@ -265,13 +310,17 @@ class Request(object):
def _load_data_and_files(self): def _load_data_and_files(self):
""" """
Parses the request content into self.DATA and self.FILES. Parses the request content into `self.data`.
""" """
if not _hasattr(self, '_content_type'): if not _hasattr(self, '_content_type'):
self._load_method_and_content_type() self._load_method_and_content_type()
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 +382,7 @@ class Request(object): ...@@ -333,6 +382,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 +400,7 @@ class Request(object): ...@@ -350,7 +400,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 +430,7 @@ class Request(object): ...@@ -380,6 +430,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
......
...@@ -5,7 +5,6 @@ it is initialized with unrendered data, instead of a pre-rendered string. ...@@ -5,7 +5,6 @@ it is initialized with unrendered data, instead of a pre-rendered string.
The appropriate renderer is called during Django's template response rendering. The appropriate renderer is called during Django's template response rendering.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import django
from django.core.handlers.wsgi import STATUS_CODE_TEXT from django.core.handlers.wsgi import STATUS_CODE_TEXT
from django.template.response import SimpleTemplateResponse from django.template.response import SimpleTemplateResponse
from django.utils import six from django.utils import six
...@@ -16,9 +15,6 @@ class Response(SimpleTemplateResponse): ...@@ -16,9 +15,6 @@ class Response(SimpleTemplateResponse):
An HttpResponse that allows its data to be rendered into An HttpResponse that allows its data to be rendered into
arbitrary media types. arbitrary media types.
""" """
# TODO: remove that once Django 1.3 isn't supported
if django.VERSION >= (1, 4):
rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_closable_objects']
def __init__(self, data=None, status=None, def __init__(self, data=None, status=None,
template_name=None, headers=None, template_name=None, headers=None,
......
...@@ -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)
...@@ -21,6 +21,7 @@ from django.conf.urls import patterns, url ...@@ -21,6 +21,7 @@ from django.conf.urls import patterns, url
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import NoReverseMatch from django.core.urlresolvers import NoReverseMatch
from rest_framework import views from rest_framework import views
from rest_framework.compat import OrderedDict
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
...@@ -277,7 +278,7 @@ class DefaultRouter(SimpleRouter): ...@@ -277,7 +278,7 @@ class DefaultRouter(SimpleRouter):
""" """
Return a view to use as the API root. Return a view to use as the API root.
""" """
api_root_dict = {} api_root_dict = OrderedDict()
list_name = self.routes[0].name list_name = self.routes[0].name
for prefix, viewset, basename in self.registry: for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename) api_root_dict[prefix] = list_name.format(basename=basename)
...@@ -286,7 +287,7 @@ class DefaultRouter(SimpleRouter): ...@@ -286,7 +287,7 @@ class DefaultRouter(SimpleRouter):
_ignore_model_permissions = True _ignore_model_permissions = True
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
ret = {} ret = OrderedDict()
for key, url_name in api_root_dict.items(): for key, url_name in api_root_dict.items():
try: try:
ret[key] = reverse( ret[key] = reverse(
......
...@@ -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',
...@@ -109,7 +110,8 @@ DEFAULTS = { ...@@ -109,7 +110,8 @@ DEFAULTS = {
# Encoding # Encoding
'UNICODE_JSON': True, 'UNICODE_JSON': True,
'COMPACT_JSON': True, 'COMPACT_JSON': True,
'COERCE_DECIMAL_TO_STRING': True 'COERCE_DECIMAL_TO_STRING': True,
'UPLOADED_FILES_USE_URL': True
} }
...@@ -121,6 +123,7 @@ IMPORT_STRINGS = ( ...@@ -121,6 +123,7 @@ 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',
......
...@@ -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;
} }
{% load rest_framework %}
{% csrf_token %}
{% for field in form %}
{% if not field.read_only %}
{% render_field field style=style %}
{% endif %}
{% endfor %}
<!-- form.non_field_errors -->
...@@ -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 %}
...@@ -142,19 +140,19 @@ ...@@ -142,19 +140,19 @@
{% if post_form %} {% if post_form %}
<ul class="nav nav-tabs form-switcher"> <ul class="nav nav-tabs form-switcher">
<li> <li>
<a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a> <a name='html-tab' href="#post-object-form" data-toggle="tab">HTML form</a>
</li> </li>
<li> <li>
<a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a> <a name='raw-tab' href="#post-generic-content-form" data-toggle="tab">Raw data</a>
</li> </li>
</ul> </ul>
{% endif %} {% endif %}
<div class="well tab-content"> <div class="well tab-content">
{% if post_form %} {% if post_form %}
<div class="tab-pane" id="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">
...@@ -166,7 +164,7 @@ ...@@ -166,7 +164,7 @@
{% endwith %} {% endwith %}
</div> </div>
{% endif %} {% endif %}
<div {% if post_form %}class="tab-pane"{% endif %} id="generic-content-form"> <div {% if post_form %}class="tab-pane"{% endif %} id="post-generic-content-form">
{% with form=raw_data_post_form %} {% with form=raw_data_post_form %}
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
<fieldset> <fieldset>
...@@ -188,18 +186,18 @@ ...@@ -188,18 +186,18 @@
{% if put_form %} {% if put_form %}
<ul class="nav nav-tabs form-switcher"> <ul class="nav nav-tabs form-switcher">
<li> <li>
<a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a> <a name='html-tab' href="#put-object-form" data-toggle="tab">HTML form</a>
</li> </li>
<li> <li>
<a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a> <a name='raw-tab' href="#put-generic-content-form" data-toggle="tab">Raw data</a>
</li> </li>
</ul> </ul>
{% endif %} {% endif %}
<div class="well tab-content"> <div class="well tab-content">
{% if put_form %} {% if put_form %}
<div class="tab-pane" id="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">
...@@ -211,7 +209,7 @@ ...@@ -211,7 +209,7 @@
</form> </form>
</div> </div>
{% endif %} {% endif %}
<div {% if put_form %}class="tab-pane"{% endif %} id="generic-content-form"> <div {% if put_form %}class="tab-pane"{% endif %} id="put-generic-content-form">
{% with form=raw_data_put_or_patch_form %} {% with form=raw_data_put_or_patch_form %}
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
<fieldset> <fieldset>
...@@ -238,13 +236,7 @@ ...@@ -238,13 +236,7 @@
{% endif %} {% endif %}
</div> </div>
<!-- END Content --> <!-- END Content -->
</div><!-- /.container -->
<footer>
{% block footer %}
<p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</p>
{% endblock %}
</footer>
</div><!-- ./wrapper --> </div><!-- ./wrapper -->
{% block script %} {% block script %}
......
{% 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 %}
{% if not nested_field.read_only %}
{% render_field nested_field style=style %}
{% endif %}
{% 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 }}" {% if style.input_type != "file" %}class="form-control"{% endif %} 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>
-->
<p>Lists are not currently supported in HTML input.</p>
</fieldset>
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