Commit f5e5ed00 by Xavier Ordoquy

Merge remote-tracking branch 'reference/master' into bugfix/1850

parents 311d315a 2647e1aa
...@@ -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
...@@ -27,7 +34,7 @@ There is a live example API for testing purposes, [available here][sandbox]. ...@@ -27,7 +34,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements # Requirements
* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4)
* Django (1.4.2+, 1.5, 1.6, 1.7) * Django (1.4.11+, 1.5.5+, 1.6, 1.7)
# Installation # Installation
...@@ -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)
...@@ -397,7 +398,7 @@ HTTP digest authentication is a widely implemented scheme that was intended to r ...@@ -397,7 +398,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
...@@ -415,6 +416,10 @@ The [HawkREST][hawkrest] library builds on the [Mohawk][mohawk] library to let y ...@@ -415,6 +416,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
...@@ -449,3 +454,4 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a ...@@ -449,3 +454,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
...@@ -29,7 +29,7 @@ The priorities for each of the given media types would be: ...@@ -29,7 +29,7 @@ The priorities for each of the given media types would be:
If the requested view was only configured with renderers for `YAML` and `HTML`, then REST framework would select whichever renderer was listed first in the `renderer_classes` list or `DEFAULT_RENDERER_CLASSES` setting. If the requested view was only configured with renderers for `YAML` and `HTML`, then REST framework would select whichever renderer was listed first in the `renderer_classes` list or `DEFAULT_RENDERER_CLASSES` setting.
For more information on the `HTTP Accept` header, see [RFC 2616][accept-header] For more information on the `HTTP Accept` header, see [RFC 2616][accept-header]
--- ---
...@@ -62,7 +62,7 @@ request when selecting the appropriate parser or renderer. ...@@ -62,7 +62,7 @@ request when selecting the appropriate parser or renderer.
Select the first parser in the `.parser_classes` list. Select the first parser in the `.parser_classes` list.
""" """
return parsers[0] return parsers[0]
def select_renderer(self, request, renderers, format_suffix): def select_renderer(self, request, renderers, format_suffix):
""" """
Select the first renderer in the `.renderer_classes` list. Select the first renderer in the `.renderer_classes` list.
......
<a class="github" href="exceptions.py"></a> source: exceptions.py
# Exceptions # Exceptions
......
<a class="github" href="fields.py"></a> source: fields.py
# Serializer fields # Serializer fields
...@@ -62,7 +62,7 @@ A dictionary of error codes to error messages. ...@@ -62,7 +62,7 @@ A dictionary of error codes to error messages.
### `widget` ### `widget`
Used only if rendering the field to HTML. Used only if rendering the field to HTML.
This argument sets the widget that should be used to render the field. For more details, and a list of available widgets, see [the Django documentation on form widgets][django-widgets]. This argument sets the widget that should be used to render the field. For more details, and a list of available widgets, see [the Django documentation on form widgets][django-widgets].
### `label` ### `label`
...@@ -274,7 +274,27 @@ Corresponds to `django.db.models.fields.FloatField`. ...@@ -274,7 +274,27 @@ Corresponds to `django.db.models.fields.FloatField`.
## DecimalField ## DecimalField
A decimal representation. A decimal representation, represented in Python by a Decimal instance.
Has two required arguments:
- `max_digits` The maximum number of digits allowed in the number. Note that this number must be greater than or equal to decimal_places.
- `decimal_places` The number of decimal places to store with the number.
For example, to validate numbers up to 999 with a resolution of 2 decimal places, you would use:
serializers.DecimalField(max_digits=5, decimal_places=2)
And to validate numbers up to anything less than one billion with a resolution of 10 decimal places:
serializers.DecimalField(max_digits=19, decimal_places=10)
This field also takes an optional argument, `coerce_to_string`. If set to `True` the representation will be output as a string. If set to `False` the representation will be left as a `Decimal` instance and the final representation will be determined by the renderer.
If unset, this will default to the same value as the `COERCE_DECIMAL_TO_STRING` setting, which is `True` unless set otherwise.
**Signature:** `DecimalField(max_digits, decimal_places, coerce_to_string=None)`
Corresponds to `django.db.models.fields.DecimalField`. Corresponds to `django.db.models.fields.DecimalField`.
......
<a class="github" href="filters.py"></a> source: filters.py
# Filtering # Filtering
...@@ -26,7 +26,7 @@ For example: ...@@ -26,7 +26,7 @@ For example:
class PurchaseList(generics.ListAPIView): class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer serializer_class = PurchaseSerializer
def get_queryset(self): def get_queryset(self):
""" """
This view should return a list of all the purchases This view should return a list of all the purchases
...@@ -38,7 +38,7 @@ For example: ...@@ -38,7 +38,7 @@ For example:
## Filtering against the URL ## Filtering against the URL
Another style of filtering might involve restricting the queryset based on some part of the URL. Another style of filtering might involve restricting the queryset based on some part of the URL.
For example if your URL config contained an entry like this: For example if your URL config contained an entry like this:
...@@ -48,7 +48,7 @@ You could then write a view that returned a purchase queryset filtered by the us ...@@ -48,7 +48,7 @@ You could then write a view that returned a purchase queryset filtered by the us
class PurchaseList(generics.ListAPIView): class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer serializer_class = PurchaseSerializer
def get_queryset(self): def get_queryset(self):
""" """
This view should return a list of all the purchases for This view should return a list of all the purchases for
...@@ -57,7 +57,7 @@ You could then write a view that returned a purchase queryset filtered by the us ...@@ -57,7 +57,7 @@ You could then write a view that returned a purchase queryset filtered by the us
username = self.kwargs['username'] username = self.kwargs['username']
return Purchase.objects.filter(purchaser__username=username) return Purchase.objects.filter(purchaser__username=username)
## Filtering against query parameters ## Filtering against query parameters
A final example of filtering the initial queryset would be to determine the initial queryset based on query parameters in the url. A final example of filtering the initial queryset would be to determine the initial queryset based on query parameters in the url.
...@@ -65,7 +65,7 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/ ...@@ -65,7 +65,7 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/
class PurchaseList(generics.ListAPIView): class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer serializer_class = PurchaseSerializer
def get_queryset(self): def get_queryset(self):
""" """
Optionally restricts the returned purchases to a given user, Optionally restricts the returned purchases to a given user,
...@@ -113,7 +113,7 @@ For instance, given the previous example, and a product with an id of `4675`, th ...@@ -113,7 +113,7 @@ For instance, given the previous example, and a product with an id of `4675`, th
http://example.com/api/products/4675/?category=clothing&max_price=10.00 http://example.com/api/products/4675/?category=clothing&max_price=10.00
## Overriding the initial queryset ## Overriding the initial queryset
Note that you can use both an overridden `.get_queryset()` and generic filtering together, and everything will work as expected. For example, if `Product` had a many-to-many relationship with `User`, named `purchase`, you might want to write a view like this: Note that you can use both an overridden `.get_queryset()` and generic filtering together, and everything will work as expected. For example, if `Product` had a many-to-many relationship with `User`, named `purchase`, you might want to write a view like this:
class PurchasedProductsList(generics.ListAPIView): class PurchasedProductsList(generics.ListAPIView):
...@@ -124,7 +124,7 @@ Note that you can use both an overridden `.get_queryset()` and generic filtering ...@@ -124,7 +124,7 @@ Note that you can use both an overridden `.get_queryset()` and generic filtering
model = Product model = Product
serializer_class = ProductSerializer serializer_class = ProductSerializer
filter_class = ProductFilter filter_class = ProductFilter
def get_queryset(self): def get_queryset(self):
user = self.request.user user = self.request.user
return user.purchase_set.all() return user.purchase_set.all()
...@@ -135,7 +135,7 @@ Note that you can use both an overridden `.get_queryset()` and generic filtering ...@@ -135,7 +135,7 @@ Note that you can use both an overridden `.get_queryset()` and generic filtering
## DjangoFilterBackend ## DjangoFilterBackend
The `DjangoFilterBackend` class supports highly customizable field filtering, using the [django-filter package][django-filter]. The `DjangoFilterBackend` class supports highly customizable field filtering, using the [django-filter package][django-filter].
To use REST framework's `DjangoFilterBackend`, first install `django-filter`. To use REST framework's `DjangoFilterBackend`, first install `django-filter`.
...@@ -216,7 +216,7 @@ This is nice, but it exposes the Django's double underscore convention as part o ...@@ -216,7 +216,7 @@ This is nice, but it exposes the Django's double underscore convention as part o
And now you can execute: And now you can execute:
http://example.com/api/products?manufacturer=foo http://example.com/api/products?manufacturer=foo
For more details on using filter sets see the [django-filter documentation][django-filter-docs]. For more details on using filter sets see the [django-filter documentation][django-filter-docs].
--- ---
...@@ -224,7 +224,7 @@ For more details on using filter sets see the [django-filter documentation][djan ...@@ -224,7 +224,7 @@ For more details on using filter sets see the [django-filter documentation][djan
**Hints & Tips** **Hints & Tips**
* By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'DEFAULT_FILTER_BACKENDS'` setting. * By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'DEFAULT_FILTER_BACKENDS'` setting.
* When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].) * When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].)
* `django-filter` supports filtering across relationships, using Django's double-underscore syntax. * `django-filter` supports filtering across relationships, using Django's double-underscore syntax.
* For Django 1.3 support, make sure to install `django-filter` version 0.5.4, as later versions drop support for 1.3. * For Django 1.3 support, make sure to install `django-filter` version 0.5.4, as later versions drop support for 1.3.
...@@ -316,7 +316,7 @@ Typically you'd instead control this by setting `order_by` on the initial querys ...@@ -316,7 +316,7 @@ Typically you'd instead control this by setting `order_by` on the initial querys
queryset = User.objects.all() queryset = User.objects.all()
serializer_class = UserSerializer serializer_class = UserSerializer
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = ('username',) ordering = ('username',)
The `ordering` attribute may be either a string or a list/tuple of strings. The `ordering` attribute may be either a string or a list/tuple of strings.
......
<a class="github" href="urlpatterns.py"></a> source: urlpatterns.py
# Format suffixes # Format suffixes
...@@ -7,7 +7,7 @@ used all the time. ...@@ -7,7 +7,7 @@ used all the time.
> >
> &mdash; Roy Fielding, [REST discuss mailing list][cite] > &mdash; Roy Fielding, [REST discuss mailing list][cite]
A common pattern for Web APIs is to use filename extensions on URLs to provide an endpoint for a given media type. For example, 'http://example.com/api/users.json' to serve a JSON representation. A common pattern for Web APIs is to use filename extensions on URLs to provide an endpoint for a given media type. For example, 'http://example.com/api/users.json' to serve a JSON representation.
Adding format-suffix patterns to each individual entry in the URLconf for your API is error-prone and non-DRY, so REST framework provides a shortcut to adding these patterns to your URLConf. Adding format-suffix patterns to each individual entry in the URLconf for your API is error-prone and non-DRY, so REST framework provides a shortcut to adding these patterns to your URLConf.
...@@ -21,7 +21,7 @@ Arguments: ...@@ -21,7 +21,7 @@ Arguments:
* **urlpatterns**: Required. A URL pattern list. * **urlpatterns**: Required. A URL pattern list.
* **suffix_required**: Optional. A boolean indicating if suffixes in the URLs should be optional or mandatory. Defaults to `False`, meaning that suffixes are optional by default. * **suffix_required**: Optional. A boolean indicating if suffixes in the URLs should be optional or mandatory. Defaults to `False`, meaning that suffixes are optional by default.
* **allowed**: Optional. A list or tuple of valid format suffixes. If not provided, a wildcard format suffix pattern will be used. * **allowed**: Optional. A list or tuple of valid format suffixes. If not provided, a wildcard format suffix pattern will be used.
Example: Example:
...@@ -33,7 +33,7 @@ Example: ...@@ -33,7 +33,7 @@ Example:
url(r'^comments/$', views.comment_list), url(r'^comments/$', views.comment_list),
url(r'^comments/(?P<pk>[0-9]+)/$', views.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'])
When using `format_suffix_patterns`, you must make sure to add the `'format'` keyword argument to the corresponding views. For example: When using `format_suffix_patterns`, you must make sure to add the `'format'` keyword argument to the corresponding views. For example:
...@@ -56,12 +56,12 @@ The name of the kwarg used may be modified by using the `FORMAT_SUFFIX_KWARG` se ...@@ -56,12 +56,12 @@ The name of the kwarg used may be modified by using the `FORMAT_SUFFIX_KWARG` se
Also note that `format_suffix_patterns` does not support descending into `include` URL patterns. Also note that `format_suffix_patterns` does not support descending into `include` URL patterns.
--- ---
## Accept headers vs. format suffixes ## Accept headers vs. format suffixes
There seems to be a view among some of the Web community that filename extensions are not a RESTful pattern, and that `HTTP Accept` headers should always be used instead. There seems to be a view among some of the Web community that filename extensions are not a RESTful pattern, and that `HTTP Accept` headers should always be used instead.
It is actually a misconception. For example, take the following quote from Roy Fielding discussing the relative merits of query parameter media-type indicators vs. file extension media-type indicators: It is actually a misconception. For example, take the following quote from Roy Fielding discussing the relative merits of query parameter media-type indicators vs. file extension media-type indicators:
&ldquo;That's why I always prefer extensions. Neither choice has anything to do with REST.&rdquo; &mdash; Roy Fielding, [REST discuss mailing list][cite2] &ldquo;That's why I always prefer extensions. Neither choice has anything to do with REST.&rdquo; &mdash; Roy Fielding, [REST discuss mailing list][cite2]
......
<a class="github" href="mixins.py"></a> source: mixins.py
<a class="github" href="generics.py"></a> generics.py
# Generic views # Generic views
...@@ -19,8 +19,8 @@ Typically when using the generic views, you'll override the view, and set severa ...@@ -19,8 +19,8 @@ Typically when using the generic views, you'll override the view, and set severa
from django.contrib.auth.models import User from django.contrib.auth.models import User
from myapp.serializers import UserSerializer from myapp.serializers import UserSerializer
from rest_framework import generics from rest_framework import generics
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
class UserList(generics.ListCreateAPIView): class UserList(generics.ListCreateAPIView):
queryset = User.objects.all() queryset = User.objects.all()
...@@ -212,8 +212,6 @@ Provides a `.list(request, *args, **kwargs)` method, that implements listing a q ...@@ -212,8 +212,6 @@ Provides a `.list(request, *args, **kwargs)` method, that implements listing a q
If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated. If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated.
If the queryset is empty this returns a `200 OK` response, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`.
## CreateModelMixin ## CreateModelMixin
Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance. Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.
...@@ -370,6 +368,20 @@ If you are using a mixin across multiple views, you can take this a step further ...@@ -370,6 +368,20 @@ If you are using a mixin across multiple views, you can take this a step further
Using custom base classes is a good option if you have custom behavior that consistently needs to be repeated across a large number of views throughout your project. Using custom base classes is a good option if you have custom behavior that consistently needs to be repeated across a large number of views throughout your project.
---
# PUT as create
Prior to version 3.0 the REST framework mixins treated `PUT` as either an update or a create operation, depending on if the object already existed or not.
Allowing `PUT` as create operations is problematic, as it necessarily exposes information about the existence or non-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>
# 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
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
> >
> &mdash; [Django documentation][cite] > &mdash; [Django documentation][cite]
REST framework includes a `PaginationSerializer` class that makes it easy to return paginated data in a way that can then be rendered to arbitrary media types. REST framework includes a `PaginationSerializer` class that makes it easy to return paginated data in a way that can then be rendered to arbitrary media types.
## Paginating basic data ## Paginating basic data
...@@ -33,7 +33,7 @@ The `context` argument of the `PaginationSerializer` class may optionally includ ...@@ -33,7 +33,7 @@ The `context` argument of the `PaginationSerializer` class may optionally includ
request = RequestFactory().get('/foobar') request = RequestFactory().get('/foobar')
serializer = PaginationSerializer(instance=page, context={'request': request}) serializer = PaginationSerializer(instance=page, context={'request': request})
serializer.data serializer.data
# {'count': 4, 'next': 'http://testserver/foobar?page=2', 'previous': None, 'results': [u'john', u'paul']} # {'count': 4, 'next': 'http://testserver/foobar?page=2', 'previous': None, 'results': [u'john', u'paul']}
We could now return that data in a `Response` object, and it would be rendered into the correct media type. We could now return that data in a `Response` object, and it would be rendered into the correct media type.
......
<a class="github" href="parsers.py"></a> source: parsers.py
# Parsers # Parsers
...@@ -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):
""" """
...@@ -197,4 +197,4 @@ The following third party packages are also available. ...@@ -197,4 +197,4 @@ The following third party packages are also available.
[juanriaza]: https://github.com/juanriaza [juanriaza]: https://github.com/juanriaza
[vbabiy]: https://github.com/vbabiy [vbabiy]: https://github.com/vbabiy
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack [djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case [djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case
\ No newline at end of file
<a class="github" href="permissions.py"></a> source: permissions.py
# Permissions # Permissions
...@@ -12,7 +12,7 @@ Permission checks are always run at the very start of the view, before any other ...@@ -12,7 +12,7 @@ Permission checks are always run at the very start of the view, before any other
## How permissions are determined ## How permissions are determined
Permissions in REST framework are always defined as a list of permission classes. Permissions in REST framework are always defined as a list of permission classes.
Before running the main body of the view each permission in the list is checked. Before running the main body of the view each permission in the list is checked.
If any permission check fails an `exceptions.PermissionDenied` exception will be raised, and the main body of the view will not run. If any permission check fails an `exceptions.PermissionDenied` exception will be raised, and the main body of the view will not run.
...@@ -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.
---
## TokenHasReadWriteScope ## TokenHasReadWriteScope
...@@ -183,11 +189,7 @@ If you need to test if a request is a read operation or a write operation, you s ...@@ -183,11 +189,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].
--- ---
...@@ -218,9 +220,9 @@ As well as global permissions, that are run against all incoming requests, you c ...@@ -218,9 +220,9 @@ As well as global permissions, that are run against all incoming requests, you c
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request, # Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests. # so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS: if request.method in permissions.SAFE_METHODS:
return True return True
# Instance must have an attribute named `owner`. # Instance must have an attribute named `owner`.
return obj.owner == request.user return obj.owner == request.user
......
<a class="github" href="relations.py"></a> source: relations.py
# Serializer relations # Serializer relations
...@@ -33,7 +33,7 @@ In order to explain the various types of relational fields, we'll use a couple o ...@@ -33,7 +33,7 @@ In order to explain the various types of relational fields, we'll use a couple o
class Meta: class Meta:
unique_together = ('album', 'order') unique_together = ('album', 'order')
order_by = 'order' order_by = 'order'
def __unicode__(self): def __unicode__(self):
return '%d: %s' % (self.order, self.title) return '%d: %s' % (self.order, self.title)
...@@ -42,10 +42,10 @@ In order to explain the various types of relational fields, we'll use a couple o ...@@ -42,10 +42,10 @@ In order to explain the various types of relational fields, we'll use a couple o
`RelatedField` may be used to represent the target of the relationship using its `__unicode__` method. `RelatedField` may be used to represent the target of the relationship using its `__unicode__` method.
For example, the following serializer. For example, the following serializer.
class AlbumSerializer(serializers.ModelSerializer): class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.RelatedField(many=True) tracks = serializers.RelatedField(many=True)
class Meta: class Meta:
model = Album model = Album
fields = ('album_name', 'artist', 'tracks') fields = ('album_name', 'artist', 'tracks')
...@@ -74,10 +74,10 @@ This field is read only. ...@@ -74,10 +74,10 @@ This field is read only.
`PrimaryKeyRelatedField` may be used to represent the target of the relationship using its primary key. `PrimaryKeyRelatedField` may be used to represent the target of the relationship using its primary key.
For example, the following serializer: For example, the following serializer:
class AlbumSerializer(serializers.ModelSerializer): class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.PrimaryKeyRelatedField(many=True, read_only=True) tracks = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta: class Meta:
model = Album model = Album
fields = ('album_name', 'artist', 'tracks') fields = ('album_name', 'artist', 'tracks')
...@@ -108,11 +108,11 @@ By default this field is read-write, although you can change this behavior using ...@@ -108,11 +108,11 @@ By default this field is read-write, although you can change this behavior using
`HyperlinkedRelatedField` may be used to represent the target of the relationship using a hyperlink. `HyperlinkedRelatedField` may be used to represent the target of the relationship using a hyperlink.
For example, the following serializer: For example, the following serializer:
class AlbumSerializer(serializers.ModelSerializer): class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.HyperlinkedRelatedField(many=True, read_only=True, tracks = serializers.HyperlinkedRelatedField(many=True, read_only=True,
view_name='track-detail') view_name='track-detail')
class Meta: class Meta:
model = Album model = Album
fields = ('album_name', 'artist', 'tracks') fields = ('album_name', 'artist', 'tracks')
...@@ -146,11 +146,11 @@ By default this field is read-write, although you can change this behavior using ...@@ -146,11 +146,11 @@ By default this field is read-write, although you can change this behavior using
`SlugRelatedField` may be used to represent the target of the relationship using a field on the target. `SlugRelatedField` may be used to represent the target of the relationship using a field on the target.
For example, the following serializer: For example, the following serializer:
class AlbumSerializer(serializers.ModelSerializer): class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.SlugRelatedField(many=True, read_only=True, tracks = serializers.SlugRelatedField(many=True, read_only=True,
slug_field='title') slug_field='title')
class Meta: class Meta:
model = Album model = Album
fields = ('album_name', 'artist', 'tracks') fields = ('album_name', 'artist', 'tracks')
...@@ -202,7 +202,7 @@ This field is always read-only. ...@@ -202,7 +202,7 @@ This field is always read-only.
**Arguments**: **Arguments**:
* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this wil be a string with the format `<model_name>-detail`. **required**. * `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this will be a string with the format `<model_name>-detail`. **required**.
* `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`. * `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument. * `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
...@@ -222,10 +222,10 @@ For example, the following serializer: ...@@ -222,10 +222,10 @@ For example, the following serializer:
class Meta: class Meta:
model = Track model = Track
fields = ('order', 'title') fields = ('order', 'title')
class AlbumSerializer(serializers.ModelSerializer): class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackSerializer(many=True) tracks = TrackSerializer(many=True)
class Meta: class Meta:
model = Album model = Album
fields = ('album_name', 'artist', 'tracks') fields = ('album_name', 'artist', 'tracks')
...@@ -262,7 +262,7 @@ For, example, we could define a relational field, to serialize a track to a cust ...@@ -262,7 +262,7 @@ For, example, we could define a relational field, to serialize a track to a cust
class AlbumSerializer(serializers.ModelSerializer): class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackListingField(many=True) tracks = TrackListingField(many=True)
class Meta: class Meta:
model = Album model = Album
fields = ('album_name', 'artist', 'tracks') fields = ('album_name', 'artist', 'tracks')
...@@ -302,7 +302,7 @@ If you have not set a related name for the reverse relationship, you'll need to ...@@ -302,7 +302,7 @@ If you have not set a related name for the reverse relationship, you'll need to
class AlbumSerializer(serializers.ModelSerializer): class AlbumSerializer(serializers.ModelSerializer):
class Meta: class Meta:
fields = ('track_set', ...) fields = ('track_set', ...)
See the Django documentation on [reverse relationships][reverse-relationships] for more details. See the Django documentation on [reverse relationships][reverse-relationships] for more details.
...@@ -315,14 +315,14 @@ For example, given the following model for a tag, which has a generic relationsh ...@@ -315,14 +315,14 @@ For example, given the following model for a tag, which has a generic relationsh
class TaggedItem(models.Model): class TaggedItem(models.Model):
""" """
Tags arbitrary model instances using a generic relation. Tags arbitrary model instances using a generic relation.
See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/ See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/
""" """
tag_name = models.SlugField() tag_name = models.SlugField()
content_type = models.ForeignKey(ContentType) content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
tagged_object = GenericForeignKey('content_type', 'object_id') tagged_object = GenericForeignKey('content_type', 'object_id')
def __unicode__(self): def __unicode__(self):
return self.tag return self.tag
...@@ -353,7 +353,7 @@ We could define a custom field that could be used to serialize tagged instances, ...@@ -353,7 +353,7 @@ We could define a custom field that could be used to serialize tagged instances,
def to_native(self, value): def to_native(self, value):
""" """
Serialize tagged objects to a simple textual representation. Serialize tagged objects to a simple textual representation.
""" """
if isinstance(value, Bookmark): if isinstance(value, Bookmark):
return 'Bookmark: ' + value.url return 'Bookmark: ' + value.url
elif isinstance(value, Note): elif isinstance(value, Note):
...@@ -366,7 +366,7 @@ If you need the target of the relationship to have a nested representation, you ...@@ -366,7 +366,7 @@ If you need the target of the relationship to have a nested representation, you
""" """
Serialize bookmark instances using a bookmark serializer, Serialize bookmark instances using a bookmark serializer,
and note instances using a note serializer. and note instances using a note serializer.
""" """
if isinstance(value, Bookmark): if isinstance(value, Bookmark):
serializer = BookmarkSerializer(value) serializer = BookmarkSerializer(value)
elif isinstance(value, Note): elif isinstance(value, Note):
...@@ -391,7 +391,7 @@ to ``True``. ...@@ -391,7 +391,7 @@ to ``True``.
## Advanced Hyperlinked fields ## Advanced Hyperlinked fields
If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`. If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`.
There are two methods you'll need to override. There are two methods you'll need to override.
...@@ -411,7 +411,7 @@ May raise an `ObjectDoesNotExist` exception. ...@@ -411,7 +411,7 @@ May raise an `ObjectDoesNotExist` exception.
### Example ### Example
For example, if all your object URLs used both a account and a slug in the the URL to reference the object, you might create a custom field like this: For example, if all your object URLs used both a account and a slug in the the URL to reference the object, you might create a custom field like this:
class CustomHyperlinkedField(serializers.HyperlinkedRelatedField): class CustomHyperlinkedField(serializers.HyperlinkedRelatedField):
def get_url(self, obj, view_name, request, format): def get_url(self, obj, view_name, request, format):
......
<a class="github" href="renderers.py"></a> source: renderers.py
# Renderers # Renderers
...@@ -74,37 +74,18 @@ If your API includes views that can serve both regular webpages and API response ...@@ -74,37 +74,18 @@ If your API includes views that can serve both regular webpages and API response
Renders the request data into `JSON`, using utf-8 encoding. Renders the request data into `JSON`, using utf-8 encoding.
Note that non-ascii characters will be rendered using JSON's `\uXXXX` character escape. For example: Note that the default style is to include unicode characters, and render the response using a compact style with no unnecessary whitespace:
{"unicode black star": "\u2605"} {"unicode black star":"★","value":999}
The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`. The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
{ {
"unicode black star": "\u2605" "unicode black star": "★",
"value": 999
} }
**.media_type**: `application/json` The default JSON encoding style can be altered using the `UNICODE_JSON` and `COMPACT_JSON` settings keys.
**.format**: `'.json'`
**.charset**: `None`
## UnicodeJSONRenderer
Renders the request data into `JSON`, using utf-8 encoding.
Note that non-ascii characters will not be character escaped. For example:
{"unicode black star": "★"}
The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
{
"unicode black star": "★"
}
Both the `JSONRenderer` and `UnicodeJSONRenderer` styles conform to [RFC 4627][rfc4627], and are syntactically valid JSON.
**.media_type**: `application/json` **.media_type**: `application/json`
...@@ -134,7 +115,7 @@ The `jsonp` approach is essentially a browser hack, and is [only appropriate for ...@@ -134,7 +115,7 @@ The `jsonp` approach is essentially a browser hack, and is [only appropriate for
## YAMLRenderer ## YAMLRenderer
Renders the request data into `YAML`. Renders the request data into `YAML`.
Requires the `pyyaml` package to be installed. Requires the `pyyaml` package to be installed.
...@@ -150,7 +131,7 @@ Note that non-ascii characters will be rendered using `\uXXXX` character escape. ...@@ -150,7 +131,7 @@ Note that non-ascii characters will be rendered using `\uXXXX` character escape.
## UnicodeYAMLRenderer ## UnicodeYAMLRenderer
Renders the request data into `YAML`. Renders the request data into `YAML`.
Requires the `pyyaml` package to be installed. Requires the `pyyaml` package to be installed.
...@@ -203,7 +184,7 @@ An example of a view that uses `TemplateHTMLRenderer`: ...@@ -203,7 +184,7 @@ An example of a view that uses `TemplateHTMLRenderer`:
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
return Response({'user': self.object}, template_name='user_detail.html') return Response({'user': self.object}, template_name='user_detail.html')
You can use `TemplateHTMLRenderer` either to return regular HTML pages using REST framework, or to return both HTML and API responses from a single endpoint. You can use `TemplateHTMLRenderer` either to return regular HTML pages using REST framework, or to return both HTML and API responses from a single endpoint.
If you're building websites that use `TemplateHTMLRenderer` along with other renderer classes, you should consider listing `TemplateHTMLRenderer` as the first class in the `renderer_classes` list, so that it will be prioritised first even for browsers that send poorly formed `ACCEPT:` headers. If you're building websites that use `TemplateHTMLRenderer` along with other renderer classes, you should consider listing `TemplateHTMLRenderer` as the first class in the `renderer_classes` list, so that it will be prioritised first even for browsers that send poorly formed `ACCEPT:` headers.
...@@ -224,7 +205,7 @@ An example of a view that uses `TemplateHTMLRenderer`: ...@@ -224,7 +205,7 @@ An example of a view that uses `TemplateHTMLRenderer`:
@api_view(('GET',)) @api_view(('GET',))
@renderer_classes((StaticHTMLRenderer,)) @renderer_classes((StaticHTMLRenderer,))
def simple_html_view(request): def simple_html_view(request):
data = '<html><body><h1>Hello, world</h1></body></html>' data = '<html><body><h1>Hello, world</h1></body></html>'
return Response(data) return Response(data)
...@@ -319,7 +300,7 @@ The following is an example plaintext renderer that will return a response with ...@@ -319,7 +300,7 @@ The following is an example plaintext renderer that will return a response with
class PlainTextRenderer(renderers.BaseRenderer): class PlainTextRenderer(renderers.BaseRenderer):
media_type = 'text/plain' media_type = 'text/plain'
format = 'txt' format = 'txt'
def render(self, data, media_type=None, renderer_context=None): def render(self, data, media_type=None, renderer_context=None):
return data.encode(self.charset) return data.encode(self.charset)
...@@ -359,7 +340,7 @@ You can do some pretty flexible things using REST framework's renderers. Some e ...@@ -359,7 +340,7 @@ You can do some pretty flexible things using REST framework's renderers. Some e
* Provide either flat or nested representations from the same endpoint, depending on the requested media type. * Provide either flat or nested representations from the same endpoint, depending on the requested media type.
* Serve both regular HTML webpages, and JSON based API responses from the same endpoints. * Serve both regular HTML webpages, and JSON based API responses from the same endpoints.
* Specify multiple types of HTML representation for API clients to use. * Specify multiple types of HTML representation for API clients to use.
* Underspecify a renderer's media type, such as using `media_type = 'image/*'`, and use the `Accept` header to vary the encoding of the response. * Underspecify a renderer's media type, such as using `media_type = 'image/*'`, and use the `Accept` header to vary the encoding of the response.
## Varying behaviour by media type ## Varying behaviour by media type
......
<a class="github" href="request.py"></a> source: request.py
# Requests # Requests
...@@ -49,6 +49,20 @@ If a client sends a request with a content-type that cannot be parsed then a `Un ...@@ -49,6 +49,20 @@ If a client sends a request with a content-type that cannot be parsed then a `Un
--- ---
# 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:
...@@ -91,7 +105,7 @@ REST framework supports a few browser enhancements such as browser-based `PUT`, ...@@ -91,7 +105,7 @@ REST framework supports a few browser enhancements such as browser-based `PUT`,
Browser-based `PUT`, `PATCH` and `DELETE` forms are transparently supported. Browser-based `PUT`, `PATCH` and `DELETE` forms are transparently supported.
For more information see the [browser enhancements documentation]. For more information see the [browser enhancements documentation].
## .content_type ## .content_type
...@@ -101,7 +115,7 @@ You won't typically need to directly access the request's content type, as you'l ...@@ -101,7 +115,7 @@ You won't typically need to directly access the request's content type, as you'l
If you do need to access the content type of the request you should use the `.content_type` property in preference to using `request.META.get('HTTP_CONTENT_TYPE')`, as it provides transparent support for browser-based non-form content. If you do need to access the content type of the request you should use the `.content_type` property in preference to using `request.META.get('HTTP_CONTENT_TYPE')`, as it provides transparent support for browser-based non-form content.
For more information see the [browser enhancements documentation]. For more information see the [browser enhancements documentation].
## .stream ## .stream
...@@ -111,7 +125,7 @@ You won't typically need to directly access the request's content, as you'll nor ...@@ -111,7 +125,7 @@ You won't typically need to directly access the request's content, as you'll nor
If you do need to access the raw content directly, you should use the `.stream` property in preference to using `request.content`, as it provides transparent support for browser-based non-form content. If you do need to access the raw content directly, you should use the `.stream` property in preference to using `request.content`, as it provides transparent support for browser-based non-form content.
For more information see the [browser enhancements documentation]. For more information see the [browser enhancements documentation].
--- ---
......
<a class="github" href="response.py"></a> source: response.py
# Responses # Responses
...@@ -90,6 +90,6 @@ The `Response` class extends `SimpleTemplateResponse`, and all the usual attribu ...@@ -90,6 +90,6 @@ The `Response` class extends `SimpleTemplateResponse`, and all the usual attribu
As with any other `TemplateResponse`, this method is called to render the serialized data of the response into the final response content. When `.render()` is called, the response content will be set to the result of calling the `.render(data, accepted_media_type, renderer_context)` method on the `accepted_renderer` instance. As with any other `TemplateResponse`, this method is called to render the serialized data of the response into the final response content. When `.render()` is called, the response content will be set to the result of calling the `.render(data, accepted_media_type, renderer_context)` method on the `accepted_renderer` instance.
You won't typically need to call `.render()` yourself, as it's handled by Django's standard response cycle. You won't typically need to call `.render()` yourself, as it's handled by Django's standard response cycle.
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/ [cite]: https://docs.djangoproject.com/en/dev/ref/template-response/
[statuscodes]: status-codes.md [statuscodes]: status-codes.md
<a class="github" href="reverse.py"></a> source: reverse.py
# Returning URLs # Returning URLs
...@@ -30,7 +30,7 @@ You should **include the request as a keyword argument** to the function, for ex ...@@ -30,7 +30,7 @@ You should **include the request as a keyword argument** to the function, for ex
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from rest_framework.views import APIView from rest_framework.views import APIView
from django.utils.timezone import now from django.utils.timezone import now
class APIRootView(APIView): class APIRootView(APIView):
def get(self, request): def get(self, request):
year = now().year year = now().year
......
<a class="github" href="routers.py"></a> source: routers.py
# Routers # Routers
...@@ -56,10 +56,10 @@ For example, given a method like this on the `UserViewSet` class: ...@@ -56,10 +56,10 @@ For example, given a method like this on the `UserViewSet` class:
from myapp.permissions import IsAdminOrIsSelf from myapp.permissions import IsAdminOrIsSelf
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
class UserViewSet(ModelViewSet): class UserViewSet(ModelViewSet):
... ...
@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf]) @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None): def set_password(self, request, pk=None):
... ...
...@@ -228,7 +228,7 @@ For another example of setting the `.routes` attribute, see the source code for ...@@ -228,7 +228,7 @@ For another example of setting the `.routes` attribute, see the source code for
## Advanced custom routers ## Advanced custom routers
If you want to provide totally custom behavior, you can override `BaseRouter` and override the `get_urls(self)` method. The method should inspect the registered viewsets and return a list of URL patterns. The registered prefix, viewset and basename tuples may be inspected by accessing the `self.registry` attribute. If you want to provide totally custom behavior, you can override `BaseRouter` and override the `get_urls(self)` method. The method should inspect the registered viewsets and return a list of URL patterns. The registered prefix, viewset and basename tuples may be inspected by accessing the `self.registry` attribute.
You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router. You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router.
......
<a class="github" href="serializers.py"></a> source: serializers.py
# Serializers # Serializers
...@@ -21,7 +21,7 @@ Let's start by creating a simple object we can use for example purposes: ...@@ -21,7 +21,7 @@ Let's start by creating a simple object we can use for example purposes:
self.email = email self.email = email
self.content = content self.content = content
self.created = created or datetime.datetime.now() self.created = created or datetime.datetime.now()
comment = Comment(email='leila@example.com', content='foo bar') comment = Comment(email='leila@example.com', content='foo bar')
We'll declare a serializer that we can use to serialize and deserialize `Comment` objects. We'll declare a serializer that we can use to serialize and deserialize `Comment` objects.
...@@ -45,7 +45,7 @@ Declaring a serializer looks very similar to declaring a form: ...@@ -45,7 +45,7 @@ Declaring a serializer looks very similar to declaring a form:
instance.content = attrs.get('content', instance.content) instance.content = attrs.get('content', instance.content)
instance.created = attrs.get('created', instance.created) instance.created = attrs.get('created', instance.created)
return instance return instance
return Comment(**attrs) return Comment(**attrs)
The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data. The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
...@@ -83,8 +83,8 @@ If you need to customize the serialized value of a particular field, you can do ...@@ -83,8 +83,8 @@ If you need to customize the serialized value of a particular field, you can do
These methods are essentially the reverse of `validate_<fieldname>` (see *Validation* below.) These methods are essentially the reverse of `validate_<fieldname>` (see *Validation* below.)
## Deserializing objects ## Deserializing objects
Deserialization is similar. First we parse a stream into Python native datatypes... Deserialization is similar. First we parse a stream into Python native datatypes...
from StringIO import StringIO from StringIO import StringIO
from rest_framework.parsers import JSONParser from rest_framework.parsers import JSONParser
...@@ -174,7 +174,7 @@ To save the deserialized objects created by a serializer, call the `.save()` met ...@@ -174,7 +174,7 @@ To save the deserialized objects created by a serializer, call the `.save()` met
The default behavior of the method is to simply call `.save()` on the deserialized object instance. You can override the default save behaviour by overriding the `.save_object(obj)` method on the serializer class. The default behavior of the method is to simply call `.save()` on the deserialized object instance. You can override the default save behaviour by overriding the `.save_object(obj)` method on the serializer class.
The generic views provided by REST framework call the `.save()` method when updating or creating entities. The generic views provided by REST framework call the `.save()` method when updating or creating entities.
## Dealing with nested objects ## Dealing with nested objects
...@@ -288,12 +288,12 @@ By default the serializer class will use the `id` key on the incoming data to de ...@@ -288,12 +288,12 @@ By default the serializer class will use the `id` key on the incoming data to de
slug = serializers.CharField(max_length=100) slug = serializers.CharField(max_length=100)
created = serializers.DateTimeField() created = serializers.DateTimeField()
... # Various other fields ... # Various other fields
def get_identity(self, data): def get_identity(self, data):
""" """
This hook is required for bulk update. This hook is required for bulk update.
We need to override the default, to use the slug as the identity. We need to override the default, to use the slug as the identity.
Note that the data has not yet been validated at this point, Note that the data has not yet been validated at this point,
so we need to deal gracefully with incorrect datatypes. so we need to deal gracefully with incorrect datatypes.
""" """
...@@ -361,7 +361,7 @@ The `depth` option should be set to an integer value that indicates the depth of ...@@ -361,7 +361,7 @@ The `depth` option should be set to an integer value that indicates the depth of
If you want to customize the way the serialization is done (e.g. using `allow_add_remove`) you'll need to define the field yourself. If you want to customize the way the serialization is done (e.g. using `allow_add_remove`) you'll need to define the field yourself.
## Specifying which fields should be read-only ## Specifying which fields should be read-only
You may wish to specify multiple fields as read-only. Instead of adding each field explicitly with the `read_only=True` attribute, you may use the `read_only_fields` Meta option, like so: You may wish to specify multiple fields as read-only. Instead of adding each field explicitly with the `read_only=True` attribute, you may use the `read_only_fields` Meta option, like so:
...@@ -371,9 +371,9 @@ You may wish to specify multiple fields as read-only. Instead of adding each fi ...@@ -371,9 +371,9 @@ You may wish to specify multiple fields as read-only. Instead of adding each fi
fields = ('id', 'account_name', 'users', 'created') fields = ('id', 'account_name', 'users', 'created')
read_only_fields = ('account_name',) read_only_fields = ('account_name',)
Model fields which have `editable=False` set, and `AutoField` fields will be set to read-only by default, and do not need to be added to the `read_only_fields` option. Model fields which have `editable=False` set, and `AutoField` fields will be set to read-only by default, and do not need to be added to the `read_only_fields` option.
## Specifying which fields should be write-only ## Specifying which fields should be write-only
You may wish to specify multiple fields as write-only. Instead of adding each field explicitly with the `write_only=True` attribute, you may use the `write_only_fields` Meta option, like so: You may wish to specify multiple fields as write-only. Instead of adding each field explicitly with the `write_only=True` attribute, you may use the `write_only_fields` Meta option, like so:
...@@ -387,12 +387,12 @@ You may wish to specify multiple fields as write-only. Instead of adding each f ...@@ -387,12 +387,12 @@ You may wish to specify multiple fields as write-only. Instead of adding each f
""" """
Instantiate a new User instance. Instantiate a new User instance.
""" """
assert instance is None, 'Cannot update users with CreateUserSerializer' assert instance is None, 'Cannot update users with CreateUserSerializer'
user = User(email=attrs['email'], username=attrs['username']) user = User(email=attrs['email'], username=attrs['username'])
user.set_password(attrs['password']) user.set_password(attrs['password'])
return user return user
## Specifying fields explicitly ## Specifying fields explicitly
You can add extra fields to a `ModelSerializer` or override the default fields by declaring fields on the class, just as you would for a `Serializer` class. You can add extra fields to a `ModelSerializer` or override the default fields by declaring fields on the class, just as you would for a `Serializer` class.
...@@ -413,6 +413,16 @@ Alternative representations include serializing using hyperlinks, serializing co ...@@ -413,6 +413,16 @@ Alternative representations include serializing using hyperlinks, serializing co
For full details see the [serializer relations][relations] documentation. For full details see the [serializer relations][relations] documentation.
## Inheritance of the 'Meta' class
The inner `Meta` class on serializers is not inherited from parent classes by default. This is the same behaviour as with Django's `Model` and `ModelForm` classes. If you want the `Meta` class to inherit from a parent class you must do so explicitly. For example:
class AccountSerializer(MyBaseSerializer):
class Meta(MyBaseSerializer.Meta):
model = Account
Typically we would recommend *not* using inheritance on inner Meta classes, but instead declaring all options explicitly.
--- ---
# HyperlinkedModelSerializer # HyperlinkedModelSerializer
...@@ -514,10 +524,10 @@ For example, if you wanted to be able to set which fields should be used by a se ...@@ -514,10 +524,10 @@ For example, if you wanted to be able to set which fields should be used by a se
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass # Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None) fields = kwargs.pop('fields', None)
# Instantiate the superclass normally # Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs) super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields: if fields:
# Drop any fields that are not specified in the `fields` argument. # Drop any fields that are not specified in the `fields` argument.
allowed = set(fields) allowed = set(fields)
...@@ -540,7 +550,7 @@ This would then allow you to do the following: ...@@ -540,7 +550,7 @@ This would then allow you to do the following:
## Customising the default fields ## Customising the default fields
The `field_mapping` attribute is a dictionary that maps model classes to serializer classes. Overriding the attribute will let you set a different set of default serializer classes. The `field_mapping` attribute is a dictionary that maps model classes to serializer classes. Overriding the attribute will let you set a different set of default serializer classes.
For more advanced customization than simply changing the default serializer class you can override various `get_<field_type>_field` methods. Doing so will allow you to customize the arguments that each serializer field is initialized with. Each of these methods may either return a field or serializer instance, or `None`. For more advanced customization than simply changing the default serializer class you can override various `get_<field_type>_field` methods. Doing so will allow you to customize the arguments that each serializer field is initialized with. Each of these methods may either return a field or serializer instance, or `None`.
......
<a class="github" href="settings.py"></a> source: settings.py
# Settings # Settings
...@@ -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`
...@@ -265,7 +265,7 @@ A format string that should be used by default for rendering the output of `Date ...@@ -265,7 +265,7 @@ A format string that should be used by default for rendering the output of `Date
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string. May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
Default: `None` Default: `'iso-8601'`
#### DATETIME_INPUT_FORMATS #### DATETIME_INPUT_FORMATS
...@@ -281,7 +281,7 @@ A format string that should be used by default for rendering the output of `Date ...@@ -281,7 +281,7 @@ A format string that should be used by default for rendering the output of `Date
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string. May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
Default: `None` Default: `'iso-8601'`
#### DATE_INPUT_FORMATS #### DATE_INPUT_FORMATS
...@@ -297,7 +297,7 @@ A format string that should be used by default for rendering the output of `Time ...@@ -297,7 +297,7 @@ A format string that should be used by default for rendering the output of `Time
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string. May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
Default: `None` Default: `'iso-8601'`
#### TIME_INPUT_FORMATS #### TIME_INPUT_FORMATS
...@@ -309,6 +309,46 @@ Default: `['iso-8601']` ...@@ -309,6 +309,46 @@ Default: `['iso-8601']`
--- ---
## Encodings
#### UNICODE_JSON
When set to `True`, JSON responses will allow unicode characters in responses. For example:
{"unicode black star":"★"}
When set to `False`, JSON responses will escape non-ascii characters, like so:
{"unicode black star":"\u2605"}
Both styles conform to [RFC 4627][rfc4627], and are syntactically valid JSON. The unicode style is preferred as being more user-friendly when inspecting API responses.
Default: `True`
#### COMPACT_JSON
When set to `True`, JSON responses will return compact representations, with no spacing after `':'` and `','` characters. For example:
{"is_admin":false,"email":"jane@example"}
When set to `False`, JSON responses will return slightly more verbose representations, like so:
{"is_admin": false, "email": "jane@example"}
The default style is to return minified responses, in line with [Heroku's API design guidelines][heroku-minified-json].
Default: `True`
#### COERCE_DECIMAL_TO_STRING
When returning decimal objects in API representations that do not support a native decimal type, it is normally best to return the value as a string. This avoids the loss of precision that occurs with binary floating point implementations.
When set to `True`, the serializer `DecimalField` class will return strings instead of `Decimal` objects. When set to `False`, serializers will return `Decimal` objects, which the default JSON encoder will return as floats.
Default: `True`
---
## View names and descriptions ## View names and descriptions
**The following settings are used to generate the view names and descriptions, as used in responses to `OPTIONS` requests, and as used in the browsable API.** **The following settings are used to generate the view names and descriptions, as used in responses to `OPTIONS` requests, and as used in the browsable API.**
...@@ -359,6 +399,12 @@ This should be a function with the following signature: ...@@ -359,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`.
...@@ -378,4 +424,6 @@ An integer of 0 or more, that may be used to specify the number of application p ...@@ -378,4 +424,6 @@ An integer of 0 or more, that may be used to specify the number of application p
Default: `None` Default: `None`
[cite]: http://www.python.org/dev/peps/pep-0020/ [cite]: http://www.python.org/dev/peps/pep-0020/
[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
[heroku-minified-json]: https://github.com/interagent/http-api-design#keep-json-minified-in-all-responses
[strftime]: http://docs.python.org/2/library/time.html#time.strftime [strftime]: http://docs.python.org/2/library/time.html#time.strftime
<a class="github" href="status.py"></a> source: status.py
# Status Codes # Status Codes
...@@ -27,7 +27,7 @@ The module also includes a set of helper functions for testing if a status code ...@@ -27,7 +27,7 @@ The module also includes a set of helper functions for testing if a status code
url = reverse('index') url = reverse('index')
response = self.client.get(url) response = self.client.get(url)
self.assertTrue(status.is_success(response.status_code)) self.assertTrue(status.is_success(response.status_code))
For more information on proper usage of HTTP status codes see [RFC 2616][rfc2616] For more information on proper usage of HTTP status codes see [RFC 2616][rfc2616]
and [RFC 6585][rfc6585]. and [RFC 6585][rfc6585].
...@@ -51,7 +51,7 @@ This class of status code indicates that the client's request was successfully r ...@@ -51,7 +51,7 @@ This class of status code indicates that the client's request was successfully r
HTTP_205_RESET_CONTENT HTTP_205_RESET_CONTENT
HTTP_206_PARTIAL_CONTENT HTTP_206_PARTIAL_CONTENT
## Redirection - 3xx ## Redirection - 3xx
This class of status code indicates that further action needs to be taken by the user agent in order to fulfill the request. This class of status code indicates that further action needs to be taken by the user agent in order to fulfill the request.
......
<a class="github" href="test.py"></a> source: test.py
# Testing # Testing
...@@ -170,7 +170,7 @@ This can be a useful shortcut if you're testing the API but don't want to have t ...@@ -170,7 +170,7 @@ This can be a useful shortcut if you're testing the API but don't want to have t
To unauthenticate subsequent requests, call `force_authenticate` setting the user and/or token to `None`. To unauthenticate subsequent requests, call `force_authenticate` setting the user and/or token to `None`.
client.force_authenticate(user=None) client.force_authenticate(user=None)
## CSRF validation ## CSRF validation
...@@ -197,7 +197,7 @@ You can use any of REST framework's test case classes as you would for the regul ...@@ -197,7 +197,7 @@ You can use any of REST framework's test case classes as you would for the regul
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
class AccountTests(APITestCase): class AccountTests(APITestCase):
def test_create_account(self): def test_create_account(self):
......
<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
...@@ -83,9 +83,9 @@ The throttle classes provided by REST framework use Django's cache backend. You ...@@ -83,9 +83,9 @@ The throttle classes provided by REST framework use Django's cache backend. You
If you need to use a cache other than `'default'`, you can do so by creating a custom throttle class and setting the `cache` attribute. For example: If you need to use a cache other than `'default'`, you can do so by creating a custom throttle class and setting the `cache` attribute. For example:
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.
--- ---
...@@ -147,15 +147,15 @@ For example, given the following views... ...@@ -147,15 +147,15 @@ For example, given the following views...
class ContactListView(APIView): class ContactListView(APIView):
throttle_scope = 'contacts' throttle_scope = 'contacts'
... ...
class ContactDetailView(ApiView): class ContactDetailView(ApiView):
throttle_scope = 'contacts' throttle_scope = 'contacts'
... ...
class UploadView(APIView): class UploadView(APIView):
throttle_scope = 'uploads' throttle_scope = 'uploads'
... ...
...and the following settings. ...and the following settings.
REST_FRAMEWORK = { REST_FRAMEWORK = {
...@@ -178,6 +178,8 @@ To create a custom throttle, override `BaseThrottle` and implement `.allow_reque ...@@ -178,6 +178,8 @@ To create a custom throttle, override `BaseThrottle` and implement `.allow_reque
Optionally you may also override the `.wait()` method. If implemented, `.wait()` should return a recommended number of seconds to wait before attempting the next request, or `None`. The `.wait()` method will only be called if `.allow_request()` has previously returned `False`. Optionally you may also override the `.wait()` method. If implemented, `.wait()` should return a recommended number of seconds to wait before attempting the next request, or `None`. The `.wait()` method will only be called if `.allow_request()` has previously returned `False`.
If the `.wait()` method is implemented and the request is throttled, then a `Retry-After` header will be included in the response.
## Example ## Example
The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests. The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests.
......
<a class="github" href="decorators.py"></a> <a class="github" href="views.py"></a> source: decorators.py
views.py
# Class Based Views # Class Based Views
...@@ -26,7 +27,7 @@ For example: ...@@ -26,7 +27,7 @@ For example:
class ListUsers(APIView): class ListUsers(APIView):
""" """
View to list all users in the system. View to list all users in the system.
* Requires token authentication. * Requires token authentication.
* Only admin users are able to access this view. * Only admin users are able to access this view.
""" """
...@@ -54,7 +55,7 @@ The following attributes control the pluggable aspects of API views. ...@@ -54,7 +55,7 @@ The following attributes control the pluggable aspects of API views.
### .permission_classes ### .permission_classes
### .content_negotiation_class ### .content_negotiation_class
## API policy instantiation methods ## API policy instantiation methods
......
<a class="github" href="viewsets.py"></a> source: viewsets.py
# ViewSets # ViewSets
......
...@@ -181,7 +181,7 @@ body{ ...@@ -181,7 +181,7 @@ body{
} }
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand{ .navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand{
color: white; color: white;
} }
.nav-list > .active > a, .navbar .navbar-inner .nav li a:hover { .nav-list > .active > a, .navbar .navbar-inner .nav li a:hover {
...@@ -190,8 +190,20 @@ body{ ...@@ -190,8 +190,20 @@ 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**: 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.
---
<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, 3.4) * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4)
* Django (1.4.2+, 1.5, 1.6, 1.7) * Django (1.4.11+, 1.5.5+, 1.6, 1.7)
The following packages are optional: The following packages are optional:
...@@ -173,6 +172,7 @@ The API guide is your complete reference manual to all the functionality provide ...@@ -173,6 +172,7 @@ The API guide is your complete reference manual to all the functionality provide
* [Serializers][serializers] * [Serializers][serializers]
* [Serializer fields][fields] * [Serializer fields][fields]
* [Serializer relations][relations] * [Serializer relations][relations]
<!--* [Validators][validators]-->
* [Authentication][authentication] * [Authentication][authentication]
* [Permissions][permissions] * [Permissions][permissions]
* [Throttling][throttling] * [Throttling][throttling]
...@@ -294,6 +294,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -294,6 +294,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[serializers]: api-guide/serializers.md [serializers]: api-guide/serializers.md
[fields]: api-guide/fields.md [fields]: api-guide/fields.md
[relations]: api-guide/relations.md [relations]: api-guide/relations.md
[validation]: api-guide/validation.md
[authentication]: api-guide/authentication.md [authentication]: api-guide/authentication.md
[permissions]: api-guide/permissions.md [permissions]: api-guide/permissions.md
[throttling]: api-guide/throttling.md [throttling]: api-guide/throttling.md
......
...@@ -42,7 +42,7 @@ The 2.2 release makes a few changes to the API, in order to make it more consist ...@@ -42,7 +42,7 @@ The 2.2 release makes a few changes to the API, in order to make it more consist
The `ManyRelatedField()` style is being deprecated in favor of a new `RelatedField(many=True)` syntax. The `ManyRelatedField()` style is being deprecated in favor of a new `RelatedField(many=True)` syntax.
For example, if a user is associated with multiple questions, which we want to represent using a primary key relationship, we might use something like the following: For example, if a user is associated with multiple questions, which we want to represent using a primary key relationship, we might use something like the following:
class UserSerializer(serializers.HyperlinkedModelSerializer): class UserSerializer(serializers.HyperlinkedModelSerializer):
questions = serializers.PrimaryKeyRelatedField(many=True) questions = serializers.PrimaryKeyRelatedField(many=True)
...@@ -58,10 +58,10 @@ The change also applies to serializers. If you have a nested serializer, you sh ...@@ -58,10 +58,10 @@ The change also applies to serializers. If you have a nested serializer, you sh
class Meta: class Meta:
model = Track model = Track
fields = ('name', 'duration') fields = ('name', 'duration')
class AlbumSerializer(serializer.ModelSerializer): class AlbumSerializer(serializer.ModelSerializer):
tracks = TrackSerializer(many=True) tracks = TrackSerializer(many=True)
class Meta: class Meta:
model = Album model = Album
fields = ('album_name', 'artist', 'tracks') fields = ('album_name', 'artist', 'tracks')
...@@ -87,7 +87,7 @@ For example, is a user account has an optional foreign key to a company, that yo ...@@ -87,7 +87,7 @@ For example, is a user account has an optional foreign key to a company, that yo
This is in line both with the rest of the serializer fields API, and with Django's `Form` and `ModelForm` API. This is in line both with the rest of the serializer fields API, and with Django's `Form` and `ModelForm` API.
Using `required` throughout the serializers API means you won't need to consider if a particular field should take `blank` or `null` arguments instead of `required`, and also means there will be more consistent behavior for how fields are treated when they are not present in the incoming data. Using `required` throughout the serializers API means you won't need to consider if a particular field should take `blank` or `null` arguments instead of `required`, and also means there will be more consistent behavior for how fields are treated when they are not present in the incoming data.
The `null=True` argument will continue to function, and will imply `required=False`, but will raise a `PendingDeprecationWarning`. The `null=True` argument will continue to function, and will imply `required=False`, but will raise a `PendingDeprecationWarning`.
......
...@@ -27,7 +27,7 @@ As an example of just how simple REST framework APIs can now be, here's an API w ...@@ -27,7 +27,7 @@ As an example of just how simple REST framework APIs can now be, here's an API w
class GroupViewSet(viewsets.ModelViewSet): class GroupViewSet(viewsets.ModelViewSet):
model = Group model = Group
# Routers provide an easy way of automatically determining the URL conf # Routers provide an easy way of automatically determining the URL conf
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'users', UserViewSet) router.register(r'users', UserViewSet)
...@@ -197,13 +197,13 @@ Usage of the old-style attributes continues to be supported, but will raise a `P ...@@ -197,13 +197,13 @@ Usage of the old-style attributes continues to be supported, but will raise a `P
For most cases APIs using model fields will behave as previously, however if you are using a custom renderer, not provided by REST framework, then you may now need to add support for rendering `Decimal` instances to your renderer implementation. For most cases APIs using model fields will behave as previously, however if you are using a custom renderer, not provided by REST framework, then you may now need to add support for rendering `Decimal` instances to your renderer implementation.
## ModelSerializers and reverse relationships ## ModelSerializers and reverse relationships
The support for adding reverse relationships to the `fields` option on a `ModelSerializer` class means that the `get_related_field` and `get_nested_field` method signatures have now changed. The support for adding reverse relationships to the `fields` option on a `ModelSerializer` class means that the `get_related_field` and `get_nested_field` method signatures have now changed.
In the unlikely event that you're providing a custom serializer class, and implementing these methods you should note the new call signature for both methods is now `(self, model_field, related_model, to_many)`. For reverse relationships `model_field` will be `None`. In the unlikely event that you're providing a custom serializer class, and implementing these methods you should note the new call signature for both methods is now `(self, model_field, related_model, to_many)`. For reverse relationships `model_field` will be `None`.
The old-style signature will continue to function but will raise a `PendingDeprecationWarning`. The old-style signature will continue to function but will raise a `PendingDeprecationWarning`.
## View names and descriptions ## View names and descriptions
...@@ -211,7 +211,7 @@ The mechanics of how the names and descriptions used in the browseable API are g ...@@ -211,7 +211,7 @@ The mechanics of how the names and descriptions used in the browseable API are g
If you've been customizing this behavior, for example perhaps to use `rst` markup for the browseable API, then you'll need to take a look at the implementation to see what updates you need to make. If you've been customizing this behavior, for example perhaps to use `rst` markup for the browseable API, then you'll need to take a look at the implementation to see what updates you need to make.
Note that the relevant methods have always been private APIs, and the docstrings called them out as intended to be deprecated. Note that the relevant methods have always been private APIs, and the docstrings called them out as intended to be deprecated.
--- ---
......
...@@ -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.
......
...@@ -10,7 +10,7 @@ If you’re building a JavaScript client to interface with your Web API, you'll ...@@ -10,7 +10,7 @@ If you’re building a JavaScript client to interface with your Web API, you'll
AJAX requests that are made within the same context as the API they are interacting with will typically use `SessionAuthentication`. This ensures that once a user has logged in, any AJAX requests made can be authenticated using the same session-based authentication that is used for the rest of the website. AJAX requests that are made within the same context as the API they are interacting with will typically use `SessionAuthentication`. This ensures that once a user has logged in, any AJAX requests made can be authenticated using the same session-based authentication that is used for the rest of the website.
AJAX requests that are made on a different site from the API they are communicating with will typically need to use a non-session-based authentication scheme, such as `TokenAuthentication`. AJAX requests that are made on a different site from the API they are communicating with will typically need to use a non-session-based authentication scheme, such as `TokenAuthentication`.
## CSRF protection ## CSRF protection
...@@ -19,7 +19,7 @@ AJAX requests that are made on a different site from the API they are communicat ...@@ -19,7 +19,7 @@ AJAX requests that are made on a different site from the API they are communicat
To guard against these type of attacks, you need to do two things: To guard against these type of attacks, you need to do two things:
1. Ensure that the 'safe' HTTP operations, such as `GET`, `HEAD` and `OPTIONS` cannot be used to alter any server-side state. 1. Ensure that the 'safe' HTTP operations, such as `GET`, `HEAD` and `OPTIONS` cannot be used to alter any server-side state.
2. Ensure that any 'unsafe' HTTP operations, such as `POST`, `PUT`, `PATCH` and `DELETE`, always require a valid CSRF token. 2. Ensure that any 'unsafe' HTTP operations, such as `POST`, `PUT`, `PATCH` and `DELETE`, always require a valid CSRF token.
If you're using `SessionAuthentication` you'll need to include valid CSRF tokens for any `POST`, `PUT`, `PATCH` or `DELETE` operations. If you're using `SessionAuthentication` you'll need to include valid CSRF tokens for any `POST`, `PUT`, `PATCH` or `DELETE` operations.
...@@ -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/
...@@ -10,9 +10,9 @@ There are many ways you can contribute to Django REST framework. We'd like it t ...@@ -10,9 +10,9 @@ There are many ways you can contribute to Django REST framework. We'd like it t
The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case. The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case.
If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular Javascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with. If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular JavaScript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with.
Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. Other really great ways you can help move the community forward include helping to answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag.
When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant. When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant.
...@@ -33,7 +33,7 @@ Some tips on good issue reporting: ...@@ -33,7 +33,7 @@ Some tips on good issue reporting:
* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing. * When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue. * Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue.
* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one. * If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one.
* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintainence overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation. * Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintenance overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation.
* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened. * Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened.
## Triaging issues ## Triaging issues
...@@ -52,7 +52,7 @@ To start developing on Django REST framework, clone the repo: ...@@ -52,7 +52,7 @@ To start developing on Django REST framework, clone the repo:
git clone git@github.com:tomchristie/django-rest-framework.git git clone git@github.com:tomchristie/django-rest-framework.git
Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you setup your editor to automatically indicated non-conforming styles. Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you set up your editor to automatically indicate non-conforming styles.
## Testing ## Testing
...@@ -71,31 +71,31 @@ To run the tests, clone the repository, and then: ...@@ -71,31 +71,31 @@ To run the tests, clone the repository, and then:
Run using a more concise output style. Run using a more concise output style.
./runtests -q ./runtests.py -q
Run the tests using a more concise output style, no coverage, no flake8. Run the tests using a more concise output style, no coverage, no flake8.
./runtests --fast ./runtests.py --fast
Don't run the flake8 code linting. Don't run the flake8 code linting.
./runtests --nolint ./runtests.py --nolint
Only run the flake8 code linting, don't run the tests. Only run the flake8 code linting, don't run the tests.
./runtests --lintonly ./runtests.py --lintonly
Run the tests for a given test case. Run the tests for a given test case.
./runtests MyTestCase ./runtests.py MyTestCase
Run the tests for a given test method. Run the tests for a given test method.
./runtests MyTestCase.test_this_method ./runtests.py MyTestCase.test_this_method
Shorter form to run the tests for a given test method. Shorter form to run the tests for a given test method.
./runtests test_this_method ./runtests.py test_this_method
Note: The test case and test method matching is fuzzy and will sometimes run other tests that contain a partial string match to the given command line input. Note: The test case and test method matching is fuzzy and will sometimes run other tests that contain a partial string match to the given command line input.
...@@ -109,7 +109,7 @@ You can also use the excellent [tox][tox] testing tool to run the tests against ...@@ -109,7 +109,7 @@ You can also use the excellent [tox][tox] testing tool to run the tests against
It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission. It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission.
It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another seperate issue without interfering with an ongoing pull requests. It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another separate issue without interfering with an ongoing pull requests.
It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests. It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests.
...@@ -117,7 +117,7 @@ GitHub's documentation for working on pull requests is [available here][pull-req ...@@ -117,7 +117,7 @@ GitHub's documentation for working on pull requests is [available here][pull-req
Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django. Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django.
Once you've made a pull request take a look at the travis build status in the GitHub interface and make sure the tests are runnning as you'd expect. Once you've made a pull request take a look at the Travis build status in the GitHub interface and make sure the tests are running as you'd expect.
![Travis status][travis-status] ![Travis status][travis-status]
...@@ -131,19 +131,19 @@ Sometimes, in order to ensure your code works on various different versions of D ...@@ -131,19 +131,19 @@ Sometimes, in order to ensure your code works on various different versions of D
The documentation for REST framework is built from the [Markdown][markdown] source files in [the docs directory][docs]. The documentation for REST framework is built from the [Markdown][markdown] source files in [the docs directory][docs].
There are many great markdown editors that make working with the documentation really easy. The [Mou editor for Mac][mou] is one such editor that comes highly recommended. There are many great Markdown editors that make working with the documentation really easy. The [Mou editor for Mac][mou] is one such editor that comes highly recommended.
## 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 documentation into the `site` 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
...@@ -153,7 +153,7 @@ Some other tips: ...@@ -153,7 +153,7 @@ Some other tips:
* Keep paragraphs reasonably short. * Keep paragraphs reasonably short.
* Use double spacing after the end of sentences. * Use double spacing after the end of sentences.
* Don't use the abbreviations such as 'e.g.' but instead use long form, such as 'For example'. * Don't use abbreviations such as 'e.g.' but instead use the long form, such as 'For example'.
## Markdown style ## Markdown style
...@@ -186,7 +186,7 @@ If you are hyperlinking to another REST framework document, you should use a rel ...@@ -186,7 +186,7 @@ If you are hyperlinking to another REST framework document, you should use a rel
[authentication]: ../api-guide/authentication.md [authentication]: ../api-guide/authentication.md
Linking in this style means you'll be able to click the hyperlink in your markdown editor to open the referenced document. When the documentation is built, these links will be converted into regular links to HTML pages. Linking in this style means you'll be able to click the hyperlink in your Markdown editor to open the referenced document. When the documentation is built, these links will be converted into regular links to HTML pages.
##### 3. Notes ##### 3. Notes
...@@ -210,7 +210,9 @@ We recommend the [`django-reusable-app`][django-reusable-app] template as a good ...@@ -210,7 +210,9 @@ We recommend the [`django-reusable-app`][django-reusable-app] template as a good
## Linking to your package ## Linking to your package
Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation. Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation. You can add your package under **Third party packages** of the API Guide section that best applies, like [Authentication][authentication] or [Permissions][permissions]. You can also link your package under the [Third Party Resources][third-party-resources] section.
We also suggest adding it to the [REST Framework][rest-framework-grid] grid on Django Packages.
[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html [cite]: http://www.w3.org/People/Berners-Lee/FAQ.html
[code-of-conduct]: https://www.djangoproject.com/conduct/ [code-of-conduct]: https://www.djangoproject.com/conduct/
...@@ -225,3 +227,7 @@ Once your package is decently documented and available on PyPI open a pull reque ...@@ -225,3 +227,7 @@ Once your package is decently documented and available on PyPI open a pull reque
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs [docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
[mou]: http://mouapp.com/ [mou]: http://mouapp.com/
[django-reusable-app]: https://github.com/dabapps/django-reusable-app [django-reusable-app]: https://github.com/dabapps/django-reusable-app
[authentication]: ../api-guide/authentication.md
[permissions]: ../api-guide/permissions.md
[third-party-resources]: third-party-resources.md
[rest-framework-grid]: https://www.djangopackages.com/grids/g/django-rest-framework/
...@@ -54,7 +54,7 @@ The title that is used in the browsable API is generated from the view class nam ...@@ -54,7 +54,7 @@ The title that is used in the browsable API is generated from the view class nam
For example, the view `UserListView`, will be named `User List` when presented in the browsable API. For example, the view `UserListView`, will be named `User List` when presented in the browsable API.
When working with viewsets, an appropriate suffix is appended to each generated view. For example, the view set `UserViewSet` will generate views named `User List` and `User Instance`. When working with viewsets, an appropriate suffix is appended to each generated view. For example, the view set `UserViewSet` will generate views named `User List` and `User Instance`.
#### Setting the description #### Setting the description
...@@ -65,9 +65,9 @@ If the python `markdown` library is installed, then [markdown syntax][markdown] ...@@ -65,9 +65,9 @@ If the python `markdown` library is installed, then [markdown syntax][markdown]
class AccountListView(views.APIView): class AccountListView(views.APIView):
""" """
Returns a list of all **active** accounts in the system. Returns a list of all **active** accounts in the system.
For more details on how accounts are activated please [see here][ref]. For more details on how accounts are activated please [see here][ref].
[ref]: http://example.com/activating-accounts [ref]: http://example.com/activating-accounts
""" """
...@@ -84,7 +84,7 @@ You can modify the response behavior to `OPTIONS` requests by overriding the `me ...@@ -84,7 +84,7 @@ You can modify the response behavior to `OPTIONS` requests by overriding the `me
def metadata(self, request): def metadata(self, request):
""" """
Don't include the view description in OPTIONS responses. Don't include the view description in OPTIONS responses.
""" """
data = super(ExampleView, self).metadata(request) data = super(ExampleView, self).metadata(request)
data.pop('description') data.pop('description')
return data return data
......
...@@ -160,4 +160,4 @@ The following individuals made a significant financial contribution to the devel ...@@ -160,4 +160,4 @@ The following individuals made a significant financial contribution to the devel
### Supporters ### Supporters
There were also almost 300 further individuals choosing to help fund the project at other levels or choosing to give anonymously. Again, thank you, thank you, thank you! There were also almost 300 further individuals choosing to help fund the project at other levels or choosing to give anonymously. Again, thank you, thank you, thank you!
\ No newline at end of file
...@@ -40,6 +40,16 @@ You can determine your currently installed version using `pip freeze`: ...@@ -40,6 +40,16 @@ 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 ### 2.4.3
**Date**: [19th September 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.3+Release%22+). **Date**: [19th September 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.3+Release%22+).
...@@ -53,7 +63,7 @@ You can determine your currently installed version using `pip freeze`: ...@@ -53,7 +63,7 @@ You can determine your currently installed version using `pip freeze`:
* Bugfix: Fix migration in `authtoken` application. * Bugfix: Fix migration in `authtoken` application.
* Bugfix: Allow selection of integer keys in nested choices. * Bugfix: Allow selection of integer keys in nested choices.
* Bugfix: Return `None` instead of `'None'` in `CharField` with `allow_none=True`. * 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: Ensure custom model fields map to equivelent serializer fields more reliably.
* Bugfix: `DjangoFilterBackend` no longer quietly changes queryset ordering. * Bugfix: `DjangoFilterBackend` no longer quietly changes queryset ordering.
### 2.4.2 ### 2.4.2
...@@ -149,7 +159,7 @@ You can determine your currently installed version using `pip freeze`: ...@@ -149,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.
......
...@@ -8,7 +8,7 @@ What it is, and why you should care. ...@@ -8,7 +8,7 @@ What it is, and why you should care.
--- ---
**Announcement:** REST framework 2 released - Tue 30th Oct 2012 **Announcement:** REST framework 2 released - Tue 30th Oct 2012
--- ---
...@@ -37,7 +37,7 @@ REST framework 2 includes a totally re-worked serialization engine, that was ini ...@@ -37,7 +37,7 @@ REST framework 2 includes a totally re-worked serialization engine, that was ini
* A declarative serialization API, that mirrors Django's `Forms`/`ModelForms` API. * A declarative serialization API, that mirrors Django's `Forms`/`ModelForms` API.
* Structural concerns are decoupled from encoding concerns. * Structural concerns are decoupled from encoding concerns.
* Able to support rendering and parsing to many formats, including both machine-readable representations and HTML forms. * Able to support rendering and parsing to many formats, including both machine-readable representations and HTML forms.
* Validation that can be mapped to obvious and comprehensive error responses. * Validation that can be mapped to obvious and comprehensive error responses.
* Serializers that support both nested, flat, and partially-nested representations. * Serializers that support both nested, flat, and partially-nested representations.
* Relationships that can be expressed as primary keys, hyperlinks, slug fields, and other custom representations. * Relationships that can be expressed as primary keys, hyperlinks, slug fields, and other custom representations.
......
...@@ -13,7 +13,7 @@ The following fall into the "required reading" category. ...@@ -13,7 +13,7 @@ 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,9 +6,9 @@ ...@@ -6,9 +6,9 @@
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
*Example of a **read-only** nested serializer. Nothing complex to worry about here.* *Example of a **read-only** nested serializer. Nothing complex to worry about here.*
...@@ -16,10 +16,10 @@ Nested data structures are easy enough to work with if they're read-only - simpl ...@@ -16,10 +16,10 @@ Nested data structures are easy enough to work with if they're read-only - simpl
class Meta: class Meta:
model = ToDoItem model = ToDoItem
fields = ('text', 'is_completed') fields = ('text', 'is_completed')
class ToDoListSerializer(serializers.ModelSerializer): class ToDoListSerializer(serializers.ModelSerializer):
items = ToDoItemSerializer(many=True, read_only=True) items = ToDoItemSerializer(many=True, read_only=True)
class Meta: class Meta:
model = ToDoList model = ToDoList
fields = ('title', 'items') fields = ('title', 'items')
...@@ -31,7 +31,7 @@ Some example output from our serializer. ...@@ -31,7 +31,7 @@ Some example output from our serializer.
'items': { 'items': {
{'text': 'Compile playlist', 'is_completed': True}, {'text': 'Compile playlist', 'is_completed': True},
{'text': 'Send invites', 'is_completed': False}, {'text': 'Send invites', 'is_completed': False},
{'text': 'Clean house', 'is_completed': False} {'text': 'Clean house', 'is_completed': False}
} }
} }
...@@ -44,4 +44,4 @@ Let's take a look at updating our nested one-to-many data structure. ...@@ -44,4 +44,4 @@ Let's take a look at updating our nested one-to-many data structure.
### Making PATCH requests ### Making PATCH requests
[cite]: http://jsonapi.org/format/#url-based-json-api [cite]: http://jsonapi.org/format/#url-based-json-api
\ No newline at end of file
...@@ -41,20 +41,7 @@ Once that's done we can create an app that we'll use to create a simple Web API. ...@@ -41,20 +41,7 @@ Once that's done we can create an app that we'll use to create a simple Web API.
python manage.py startapp snippets python manage.py startapp snippets
The simplest way to get up and running will probably be to use an `sqlite3` database for the tutorial. Edit the `tutorial/settings.py` file, and set the default database `"ENGINE"` to `"sqlite3"`, and `"NAME"` to `"tmp.db"`. We'll need to add our new `snippets` app and the `rest_framework` app to `INSTALLED_APPS`. Let's edit the `tutorial/settings.py` file:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'tmp.db',
'USER': '',
'PASSWORD': '',
'HOST': '',
'PORT': '',
}
}
We'll also need to add our new `snippets` app and the `rest_framework` app to `INSTALLED_APPS`.
INSTALLED_APPS = ( INSTALLED_APPS = (
... ...
...@@ -72,7 +59,7 @@ Okay, we're ready to roll. ...@@ -72,7 +59,7 @@ Okay, we're ready to roll.
## Creating a model to work with ## Creating a model to work with
For the purposes of this tutorial we're going to start by creating a simple `Snippet` model that is used to store code snippets. Go ahead and edit the `snippets` app's `models.py` file. Note: Good programming practices include comments. Although you will find them in our repository version of this tutorial code, we have omitted them here to focus on the code itself. For the purposes of this tutorial we're going to start by creating a simple `Snippet` model that is used to store code snippets. Go ahead and edit the `snippets/models.py` file. Note: Good programming practices include comments. Although you will find them in our repository version of this tutorial code, we have omitted them here to focus on the code itself.
from django.db import models from django.db import models
from pygments.lexers import get_all_lexers from pygments.lexers import get_all_lexers
...@@ -98,9 +85,10 @@ For the purposes of this tutorial we're going to start by creating a simple `Sni ...@@ -98,9 +85,10 @@ For the purposes of this tutorial we're going to start by creating a simple `Sni
class Meta: class Meta:
ordering = ('created',) ordering = ('created',)
Don't forget to sync the database for the first time. We'll also need to create an initial migration for our snippet model, and sync the database for the first time.
python manage.py syncdb python manage.py makemigrations snippets
python manage.py migrate
## Creating a Serializer class ## Creating a Serializer class
...@@ -112,42 +100,41 @@ The first thing we need to get started on our Web API is to provide a way of ser ...@@ -112,42 +100,41 @@ 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.
## Working with Serializers ## Working with Serializers
...@@ -219,6 +206,24 @@ Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer` ...@@ -219,6 +206,24 @@ Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer`
model = Snippet model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style') fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
Once nice property that serializers have is that you can inspect all the fields an serializer instance, by printing it's representation. Open the Django shell with `python manange.py shell`, then try the following:
>>> from snippets.serializers import SnippetSerializer
>>> serializer = SnippetSerializer()
>>> print repr(serializer) # In python 3 use `print(repr(serializer))`
SnippetSerializer():
id = IntegerField(label='ID', read_only=True)
title = CharField(allow_blank=True, max_length=100, required=False)
code = CharField(style={'type': 'textarea'})
linenos = BooleanField(required=False)
language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')...
style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')...
It's important to remember that `ModelSerializer` classes don't do anything particularly magically, they are simply a shortcut to creating a serializer class with:
* An automatically determined set of fields.
* Simple default implementations for the `create()` and `update()` methods.
## Writing regular Django views using our Serializer ## Writing regular Django views using our Serializer
Let's see how we can write some API views using our new Serializer class. Let's see how we can write some API views using our new Serializer class.
......
...@@ -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
......
...@@ -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
......
...@@ -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({
......
...@@ -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>
<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>Sponsored by <a href="http://dabapps.com/">DabApps</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']
google_analytics: ['UA-18852272-2', 'django-rest-framework.org']
...@@ -8,8 +8,8 @@ flake8==2.2.2 ...@@ -8,8 +8,8 @@ flake8==2.2.2
markdown>=2.1.0 markdown>=2.1.0
PyYAML>=3.10 PyYAML>=3.10
defusedxml>=0.3 defusedxml>=0.3
django-guardian==1.2.4
django-filter>=0.5.4 django-filter>=0.5.4
django-oauth-plus>=2.2.1 django-oauth-plus>=2.2.1
oauth2>=1.5.211 oauth2>=1.5.211
django-oauth2-provider>=0.2.4 django-oauth2-provider>=0.2.4
Pillow==2.3.0
...@@ -8,7 +8,7 @@ ______ _____ _____ _____ __ ...@@ -8,7 +8,7 @@ ______ _____ _____ _____ __
""" """
__title__ = 'Django REST framework' __title__ = 'Django REST framework'
__version__ = '2.4.3' __version__ = '3.0.0'
__author__ = 'Tom Christie' __author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause' __license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2014 Tom Christie' __copyright__ = 'Copyright 2011-2014 Tom Christie'
......
...@@ -129,7 +129,7 @@ class SessionAuthentication(BaseAuthentication): ...@@ -129,7 +129,7 @@ class SessionAuthentication(BaseAuthentication):
reason = CSRFCheck().process_view(request, None, (), {}) reason = CSRFCheck().process_view(request, None, (), {})
if reason: if reason:
# CSRF failed, bail with explicit error message # CSRF failed, bail with explicit error message
raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason) raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
class TokenAuthentication(BaseAuthentication): class TokenAuthentication(BaseAuthentication):
......
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import exceptions, serializers
class AuthTokenSerializer(serializers.Serializer): class AuthTokenSerializer(serializers.Serializer):
...@@ -18,12 +18,13 @@ class AuthTokenSerializer(serializers.Serializer): ...@@ -18,12 +18,13 @@ class AuthTokenSerializer(serializers.Serializer):
if user: if user:
if not user.is_active: if not user.is_active:
msg = _('User account is disabled.') msg = _('User account is disabled.')
raise serializers.ValidationError(msg) raise exceptions.ValidationError(msg)
attrs['user'] = user
return attrs
else: else:
msg = _('Unable to log in with provided credentials.') msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg) raise exceptions.ValidationError(msg)
else: else:
msg = _('Must include "username" and "password"') msg = _('Must include "username" and "password"')
raise serializers.ValidationError(msg) raise exceptions.ValidationError(msg)
attrs['user'] = user
return attrs
...@@ -16,9 +16,10 @@ class ObtainAuthToken(APIView): ...@@ -16,9 +16,10 @@ class ObtainAuthToken(APIView):
model = Token model = Token
def post(self, request): def post(self, request):
serializer = self.serializer_class(data=request.DATA) serializer = self.serializer_class(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.object['user']) user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key}) return Response({'token': token.key})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
......
...@@ -5,11 +5,12 @@ versions of django/python, and compatibility wrappers around optional packages. ...@@ -5,11 +5,12 @@ versions of django/python, and compatibility wrappers around optional packages.
# flake8: noqa # flake8: noqa
from __future__ import unicode_literals from __future__ import unicode_literals
import django
import inspect
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.conf import settings from django.conf import settings
from django.utils import six from django.utils import six
import django
import inspect
# Handle django.utils.encoding rename in 1.5 onwards. # Handle django.utils.encoding rename in 1.5 onwards.
...@@ -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
...@@ -39,6 +50,17 @@ except ImportError: ...@@ -39,6 +50,17 @@ except ImportError:
django_filters = None django_filters = None
if django.VERSION >= (1, 6):
def clean_manytomany_helptext(text):
return text
else:
# Up to version 1.5 many to many fields automatically suffix
# the `help_text` attribute with hardcoded text.
def clean_manytomany_helptext(text):
if text.endswith(' Hold down "Control", or "Command" on a Mac, to select more than one.'):
text = text[:-69]
return text
# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS # Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
# Fixes (#1712). We keep the try/except for the test suite. # Fixes (#1712). We keep the try/except for the test suite.
guardian = None guardian = None
...@@ -73,15 +95,6 @@ except ImportError: ...@@ -73,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:
...@@ -110,6 +123,62 @@ else: ...@@ -110,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']
...@@ -133,12 +202,12 @@ class RequestFactory(DjangoRequestFactory): ...@@ -133,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):
...@@ -232,6 +301,15 @@ except ImportError: ...@@ -232,6 +301,15 @@ except ImportError:
oauth2_constants = None oauth2_constants = None
provider_now = None provider_now = None
# `seperators` argument to `json.dumps()` differs between 2.x and 3.x
# See: http://bugs.python.org/issue22767
if six.PY3:
SHORT_SEPARATORS = (',', ':')
LONG_SEPARATORS = (', ', ': ')
else:
SHORT_SEPARATORS = (b',', b':')
LONG_SEPARATORS = (b', ', b': ')
# Handle lazy strings across Py2/Py3 # Handle lazy strings across Py2/Py3
from django.utils.functional import Promise from django.utils.functional import Promise
......
...@@ -10,7 +10,6 @@ from __future__ import unicode_literals ...@@ -10,7 +10,6 @@ from __future__ import unicode_literals
from django.utils import six from django.utils import six
from rest_framework.views import APIView from rest_framework.views import APIView
import types import types
import warnings
def api_view(http_method_names): def api_view(http_method_names):
...@@ -130,37 +129,3 @@ def list_route(methods=['get'], **kwargs): ...@@ -130,37 +129,3 @@ def list_route(methods=['get'], **kwargs):
func.kwargs = kwargs func.kwargs = kwargs
return func return func
return decorator return decorator
# These are now pending deprecation, in favor of `detail_route` and `list_route`.
def link(**kwargs):
"""
Used to mark a method on a ViewSet that should be routed for detail GET requests.
"""
msg = 'link is pending deprecation. Use detail_route instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
def decorator(func):
func.bind_to_methods = ['get']
func.detail = True
func.kwargs = kwargs
return func
return decorator
def action(methods=['post'], **kwargs):
"""
Used to mark a method on a ViewSet that should be routed for detail POST requests.
"""
msg = 'action is pending deprecation. Use detail_route instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
def decorator(func):
func.bind_to_methods = methods
func.detail = True
func.kwargs = kwargs
return func
return decorator
...@@ -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 = '' 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's Accept header" default_detail = _('Could not satisfy the request Accept header')
def __init__(self, detail=None, available_renderers=None): def __init__(self, detail=None, available_renderers=None):
self.detail = detail or self.default_detail 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()
......
"""
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,40 +6,9 @@ which allows mixin classes to be composed in interesting ways. ...@@ -6,40 +6,9 @@ which allows mixin classes to be composed in interesting ways.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.exceptions import ValidationError
from django.http import Http404
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.request import clone_request
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
import warnings
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
"""
Given a model instance, and an optional pk and slug field,
return the full list of all other field names on that model.
For use when performing full_clean on a model instance,
so we only clean the required fields.
"""
include = []
if pk:
# Deprecated
pk_field = obj._meta.pk
while pk_field.rel:
pk_field = pk_field.rel.to._meta.pk
include.append(pk_field.name)
if slug_field:
# Deprecated
include.append(slug_field)
if lookup_field and lookup_field != 'pk':
include.append(lookup_field)
return [field.name for field in obj._meta.fields if field.name not in include]
class CreateModelMixin(object): class CreateModelMixin(object):
...@@ -47,17 +16,14 @@ class CreateModelMixin(object): ...@@ -47,17 +16,14 @@ class CreateModelMixin(object):
Create a model instance. Create a model instance.
""" """
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA, files=request.FILES) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
if serializer.is_valid(): def perform_create(self, serializer):
self.pre_save(serializer.object) serializer.save()
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def get_success_headers(self, data): def get_success_headers(self, data):
try: try:
...@@ -70,31 +36,13 @@ class ListModelMixin(object): ...@@ -70,31 +36,13 @@ class ListModelMixin(object):
""" """
List a queryset. List a queryset.
""" """
empty_error = "Empty list and '%(class_name)s.allow_empty' is False."
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
self.object_list = self.filter_queryset(self.get_queryset()) instance = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(instance)
# Default is to allow empty querysets. This can be altered by setting
# `.allow_empty = False`, to raise 404 errors on empty querysets.
if not self.allow_empty and not self.object_list:
warnings.warn(
'The `allow_empty` parameter is deprecated. '
'To use `allow_empty=False` style behavior, You should override '
'`get_queryset()` and explicitly raise a 404 on empty querysets.',
DeprecationWarning
)
class_name = self.__class__.__name__
error_msg = self.empty_error % {'class_name': class_name}
raise Http404(error_msg)
# Switch between paginated or standard style responses
page = self.paginate_queryset(self.object_list)
if page is not None: if page is not None:
serializer = self.get_pagination_serializer(page) serializer = self.get_pagination_serializer(page)
else: else:
serializer = self.get_serializer(self.object_list, many=True) serializer = self.get_serializer(instance, many=True)
return Response(serializer.data) return Response(serializer.data)
...@@ -103,8 +51,8 @@ class RetrieveModelMixin(object): ...@@ -103,8 +51,8 @@ class RetrieveModelMixin(object):
Retrieve a model instance. Retrieve a model instance.
""" """
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
self.object = self.get_object() instance = self.get_object()
serializer = self.get_serializer(self.object) serializer = self.get_serializer(instance)
return Response(serializer.data) return Response(serializer.data)
...@@ -114,83 +62,28 @@ class UpdateModelMixin(object): ...@@ -114,83 +62,28 @@ class UpdateModelMixin(object):
""" """
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False) partial = kwargs.pop('partial', False)
self.object = self.get_object_or_none() instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer = self.get_serializer(self.object, data=request.DATA, serializer.is_valid(raise_exception=True)
files=request.FILES, partial=partial) self.perform_update(serializer)
return Response(serializer.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try:
self.pre_save(serializer.object)
except ValidationError as err:
# full_clean on model instance may be called in pre_save,
# so we have to handle eventual errors.
return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
if self.object is None:
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
self.object = serializer.save(force_update=True) def perform_update(self, serializer):
self.post_save(self.object, created=False) serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True kwargs['partial'] = True
return self.update(request, *args, **kwargs) return self.update(request, *args, **kwargs)
def get_object_or_none(self):
try:
return self.get_object()
except Http404:
if self.request.method == 'PUT':
# For PUT-as-create operation, we need to ensure that we have
# relevant permissions, as if this was a POST request. This
# will either raise a PermissionDenied exception, or simply
# return None.
self.check_permissions(clone_request(self.request, 'POST'))
else:
# PATCH requests where the object does not exist should still
# return a 404 response.
raise
def pre_save(self, obj):
"""
Set any attributes on the object that are implicit in the request.
"""
# pk and/or slug attributes are implicit in the URL.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
lookup = self.kwargs.get(lookup_url_kwarg, None)
pk = self.kwargs.get(self.pk_url_kwarg, None)
slug = self.kwargs.get(self.slug_url_kwarg, None)
slug_field = slug and self.slug_field or None
if lookup:
setattr(obj, self.lookup_field, lookup)
if pk:
setattr(obj, 'pk', pk)
if slug:
setattr(obj, slug_field, slug)
# Ensure we clean the attributes so that we don't eg return integer
# pk using a string representation, as provided by the url conf kwarg.
if hasattr(obj, 'full_clean'):
exclude = _get_validation_exclusions(obj, pk, slug_field, self.lookup_field)
obj.full_clean(exclude)
class DestroyModelMixin(object): class DestroyModelMixin(object):
""" """
Destroy a model instance. Destroy a model instance.
""" """
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
obj = self.get_object() instance = self.get_object()
self.pre_delete(obj) self.perform_destroy(instance)
obj.delete()
self.post_delete(obj)
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
def perform_destroy(self, instance):
instance.delete()
...@@ -38,7 +38,7 @@ class DefaultContentNegotiation(BaseContentNegotiation): ...@@ -38,7 +38,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
""" """
# Allow URL style format override. eg. "?format=json # Allow URL style format override. eg. "?format=json
format_query_param = self.settings.URL_FORMAT_OVERRIDE format_query_param = self.settings.URL_FORMAT_OVERRIDE
format = format_suffix or request.QUERY_PARAMS.get(format_query_param) format = format_suffix or request.query_params.get(format_query_param)
if format: if format:
renderers = self.filter_renderers(renderers, format) renderers = self.filter_renderers(renderers, format)
...@@ -87,5 +87,5 @@ class DefaultContentNegotiation(BaseContentNegotiation): ...@@ -87,5 +87,5 @@ class DefaultContentNegotiation(BaseContentNegotiation):
Allows URL style accept override. eg. "?accept=application/json" Allows URL style accept override. eg. "?accept=application/json"
""" """
header = request.META.get('HTTP_ACCEPT', '*/*') header = request.META.get('HTTP_ACCEPT', '*/*')
header = request.QUERY_PARAMS.get(self.settings.URL_ACCEPT_OVERRIDE, header) header = request.query_params.get(self.settings.URL_ACCEPT_OVERRIDE, header)
return [token.strip() for token in header.split(',')] return [token.strip() for token in header.split(',')]
...@@ -13,7 +13,7 @@ class NextPageField(serializers.Field): ...@@ -13,7 +13,7 @@ class NextPageField(serializers.Field):
""" """
page_field = 'page' page_field = 'page'
def to_native(self, value): def to_representation(self, value):
if not value.has_next(): if not value.has_next():
return None return None
page = value.next_page_number() page = value.next_page_number()
...@@ -28,7 +28,7 @@ class PreviousPageField(serializers.Field): ...@@ -28,7 +28,7 @@ class PreviousPageField(serializers.Field):
""" """
page_field = 'page' page_field = 'page'
def to_native(self, value): def to_representation(self, value):
if not value.has_previous(): if not value.has_previous():
return None return None
page = value.previous_page_number() page = value.previous_page_number()
...@@ -37,7 +37,7 @@ class PreviousPageField(serializers.Field): ...@@ -37,7 +37,7 @@ class PreviousPageField(serializers.Field):
return replace_query_param(url, self.page_field, page) return replace_query_param(url, self.page_field, page)
class DefaultObjectSerializer(serializers.Field): class DefaultObjectSerializer(serializers.ReadOnlyField):
""" """
If no object serializer is specified, then this serializer will be applied If no object serializer is specified, then this serializer will be applied
as the default. as the default.
...@@ -49,25 +49,11 @@ class DefaultObjectSerializer(serializers.Field): ...@@ -49,25 +49,11 @@ class DefaultObjectSerializer(serializers.Field):
super(DefaultObjectSerializer, self).__init__(source=source) super(DefaultObjectSerializer, self).__init__(source=source)
class PaginationSerializerOptions(serializers.SerializerOptions):
"""
An object that stores the options that may be provided to a
pagination serializer by using the inner `Meta` class.
Accessible on the instance as `serializer.opts`.
"""
def __init__(self, meta):
super(PaginationSerializerOptions, self).__init__(meta)
self.object_serializer_class = getattr(meta, 'object_serializer_class',
DefaultObjectSerializer)
class BasePaginationSerializer(serializers.Serializer): class BasePaginationSerializer(serializers.Serializer):
""" """
A base class for pagination serializers to inherit from, A base class for pagination serializers to inherit from,
to make implementing custom serializers more easy. to make implementing custom serializers more easy.
""" """
_options_class = PaginationSerializerOptions
results_field = 'results' results_field = 'results'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
...@@ -76,22 +62,22 @@ class BasePaginationSerializer(serializers.Serializer): ...@@ -76,22 +62,22 @@ class BasePaginationSerializer(serializers.Serializer):
""" """
super(BasePaginationSerializer, self).__init__(*args, **kwargs) super(BasePaginationSerializer, self).__init__(*args, **kwargs)
results_field = self.results_field results_field = self.results_field
object_serializer = self.opts.object_serializer_class
if 'context' in kwargs: try:
context_kwarg = {'context': kwargs['context']} object_serializer = self.Meta.object_serializer_class
else: except AttributeError:
context_kwarg = {} object_serializer = DefaultObjectSerializer
self.fields[results_field] = object_serializer(source='object_list', self.fields[results_field] = serializers.ListSerializer(
many=True, child=object_serializer(),
**context_kwarg) source='object_list'
)
class PaginationSerializer(BasePaginationSerializer): class PaginationSerializer(BasePaginationSerializer):
""" """
A default implementation of a pagination serializer. A default implementation of a pagination serializer.
""" """
count = serializers.Field(source='paginator.count') count = serializers.ReadOnlyField(source='paginator.count')
next = NextPageField(source='*') next = NextPageField(source='*')
previous = PreviousPageField(source='*') previous = PreviousPageField(source='*')
...@@ -5,6 +5,7 @@ They give us a generic way of being able to handle various media types ...@@ -5,6 +5,7 @@ They give us a generic way of being able to handle various media types
on the request, such as form content or json encoded data. on the request, such as form content or json encoded data.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.core.files.uploadhandler import StopFutureHandlers from django.core.files.uploadhandler import StopFutureHandlers
from django.http import QueryDict from django.http import QueryDict
...@@ -48,7 +49,7 @@ class JSONParser(BaseParser): ...@@ -48,7 +49,7 @@ class JSONParser(BaseParser):
""" """
media_type = 'application/json' media_type = 'application/json'
renderer_class = renderers.UnicodeJSONRenderer renderer_class = renderers.JSONRenderer
def parse(self, stream, media_type=None, parser_context=None): def parse(self, stream, media_type=None, parser_context=None):
""" """
...@@ -132,7 +133,7 @@ class MultiPartParser(BaseParser): ...@@ -132,7 +133,7 @@ class MultiPartParser(BaseParser):
data, files = parser.parse() data, files = parser.parse()
return DataAndFiles(data, files) return DataAndFiles(data, files)
except MultiPartParserError as exc: except MultiPartParserError as exc:
raise ParseError('Multipart form parse error - %s' % str(exc)) raise ParseError('Multipart form parse error - %s' % six.text_type(exc))
class XMLParser(BaseParser): class XMLParser(BaseParser):
......
...@@ -4,7 +4,7 @@ The Request class is used as a wrapper around the standard request object. ...@@ -4,7 +4,7 @@ The Request class is used as a wrapper around the standard request object.
The wrapped request then offers a richer API, in particular : The wrapped request then offers a richer API, in particular :
- content automatically parsed according to `Content-Type` header, - content automatically parsed according to `Content-Type` header,
and available as `request.DATA` and available as `request.data`
- full support of PUT method, including support for file uploads - full support of PUT method, including support for file uploads
- form overloading of HTTP method, content type and content - form overloading of HTTP method, content type and content
""" """
...@@ -13,10 +13,12 @@ from django.conf import settings ...@@ -13,10 +13,12 @@ from django.conf import settings
from django.http import QueryDict from django.http import QueryDict
from django.http.multipartparser import parse_header from django.http.multipartparser import parse_header
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.datastructures import MergeDict as DjangoMergeDict
from rest_framework import HTTP_HEADER_ENCODING from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework.compat import BytesIO from rest_framework.compat import BytesIO
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
import warnings
def is_form_media_type(media_type): def is_form_media_type(media_type):
...@@ -58,6 +60,15 @@ class override_method(object): ...@@ -58,6 +60,15 @@ class override_method(object):
self.view.action = self.action self.view.action = self.action
class MergeDict(DjangoMergeDict, dict):
"""
Using this as a workaround until the parsers API is properly
addressed in 3.1.
"""
def __init__(self, *dicts):
self.dicts = dicts
class Empty(object): class Empty(object):
""" """
Placeholder for unset attributes. Placeholder for unset attributes.
...@@ -82,6 +93,7 @@ def clone_request(request, method): ...@@ -82,6 +93,7 @@ def clone_request(request, method):
parser_context=request.parser_context) parser_context=request.parser_context)
ret._data = request._data ret._data = request._data
ret._files = request._files ret._files = request._files
ret._full_data = request._full_data
ret._content_type = request._content_type ret._content_type = request._content_type
ret._stream = request._stream ret._stream = request._stream
ret._method = method ret._method = method
...@@ -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
...@@ -272,6 +317,10 @@ class Request(object): ...@@ -272,6 +317,10 @@ class Request(object):
if not _hasattr(self, '_data'): if not _hasattr(self, '_data'):
self._data, self._files = self._parse() self._data, self._files = self._parse()
if self._files:
self._full_data = MergeDict(self._data, self._files)
else:
self._full_data = self._data
def _load_method_and_content_type(self): def _load_method_and_content_type(self):
""" """
...@@ -333,6 +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
......
...@@ -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',
...@@ -77,6 +78,7 @@ DEFAULTS = { ...@@ -77,6 +78,7 @@ DEFAULTS = {
# Exception handling # Exception handling
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler', 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
'NON_FIELD_ERRORS_KEY': 'non_field_errors',
# Testing # Testing
'TEST_REQUEST_RENDERER_CLASSES': ( 'TEST_REQUEST_RENDERER_CLASSES': (
...@@ -96,24 +98,20 @@ DEFAULTS = { ...@@ -96,24 +98,20 @@ DEFAULTS = {
'URL_FIELD_NAME': 'url', 'URL_FIELD_NAME': 'url',
# Input and output formats # Input and output formats
'DATE_INPUT_FORMATS': ( 'DATE_FORMAT': ISO_8601,
ISO_8601, 'DATE_INPUT_FORMATS': (ISO_8601,),
),
'DATE_FORMAT': None,
'DATETIME_INPUT_FORMATS': ( 'DATETIME_FORMAT': ISO_8601,
ISO_8601, 'DATETIME_INPUT_FORMATS': (ISO_8601,),
),
'DATETIME_FORMAT': None,
'TIME_INPUT_FORMATS': ( 'TIME_FORMAT': ISO_8601,
ISO_8601, 'TIME_INPUT_FORMATS': (ISO_8601,),
),
'TIME_FORMAT': None,
# Pending deprecation
'FILTER_BACKEND': None,
# Encoding
'UNICODE_JSON': True,
'COMPACT_JSON': True,
'COERCE_DECIMAL_TO_STRING': True,
'UPLOADED_FILES_USE_URL': True
} }
...@@ -125,11 +123,11 @@ IMPORT_STRINGS = ( ...@@ -125,11 +123,11 @@ IMPORT_STRINGS = (
'DEFAULT_PERMISSION_CLASSES', 'DEFAULT_PERMISSION_CLASSES',
'DEFAULT_THROTTLE_CLASSES', 'DEFAULT_THROTTLE_CLASSES',
'DEFAULT_CONTENT_NEGOTIATION_CLASS', 'DEFAULT_CONTENT_NEGOTIATION_CLASS',
'DEFAULT_METADATA_CLASS',
'DEFAULT_MODEL_SERIALIZER_CLASS', 'DEFAULT_MODEL_SERIALIZER_CLASS',
'DEFAULT_PAGINATION_SERIALIZER_CLASS', 'DEFAULT_PAGINATION_SERIALIZER_CLASS',
'DEFAULT_FILTER_BACKENDS', 'DEFAULT_FILTER_BACKENDS',
'EXCEPTION_HANDLER', 'EXCEPTION_HANDLER',
'FILTER_BACKEND',
'TEST_REQUEST_RENDERER_CLASSES', 'TEST_REQUEST_RENDERER_CLASSES',
'UNAUTHENTICATED_USER', 'UNAUTHENTICATED_USER',
'UNAUTHENTICATED_TOKEN', 'UNAUTHENTICATED_TOKEN',
...@@ -196,15 +194,9 @@ class APISettings(object): ...@@ -196,15 +194,9 @@ class APISettings(object):
if val and attr in self.import_strings: if val and attr in self.import_strings:
val = perform_import(val, attr) val = perform_import(val, attr)
self.validate_setting(attr, val)
# Cache the result # Cache the result
setattr(self, attr, val) setattr(self, attr, val)
return val return val
def validate_setting(self, attr, val):
if attr == 'FILTER_BACKEND' and val is not None:
# Make sure we can initialize the class
val()
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
...@@ -10,6 +10,12 @@ a single block in the template. ...@@ -10,6 +10,12 @@ a single block in the template.
background: transparent; background: transparent;
border-top-color: transparent; border-top-color: transparent;
padding-top: 0; padding-top: 0;
text-align: right;
}
#generic-content-form textarea {
font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
font-size: 80%;
} }
.navbar-inverse .brand a { .navbar-inverse .brand a {
...@@ -29,7 +35,7 @@ a single block in the template. ...@@ -29,7 +35,7 @@ a single block in the template.
z-index: 3; z-index: 3;
} }
.navbar .navbar-inner { .navbar {
background: #2C2C2C; background: #2C2C2C;
color: white; color: white;
border: none; border: none;
...@@ -37,7 +43,7 @@ a single block in the template. ...@@ -37,7 +43,7 @@ a single block in the template.
border-radius: 0px; border-radius: 0px;
} }
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover { .navbar .nav li, .navbar .nav li a, .navbar .brand:hover {
color: white; color: white;
} }
...@@ -45,11 +51,11 @@ a single block in the template. ...@@ -45,11 +51,11 @@ a single block in the template.
background: #2C2C2C; background: #2C2C2C;
} }
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li { .navbar .dropdown-menu li a, .navbar .dropdown-menu li {
color: #A30000; color: #A30000;
} }
.navbar .navbar-inner .dropdown-menu li a:hover { .navbar .dropdown-menu li a:hover {
background: #EEEEEE; background: #EEEEEE;
color: #C20000; color: #C20000;
} }
...@@ -61,10 +67,10 @@ html { ...@@ -61,10 +67,10 @@ html {
background: none; background: none;
} }
body, .navbar .navbar-inner .container-fluid { /*body, .navbar .container-fluid {
max-width: 1150px; max-width: 1150px;
margin: 0 auto; margin: 0 auto;
} }*/
body { body {
background: url("../img/grid.png") repeat-x; background: url("../img/grid.png") repeat-x;
...@@ -109,10 +115,6 @@ html, body { ...@@ -109,10 +115,6 @@ html, body {
margin-bottom: 0; margin-bottom: 0;
} }
.well form .help-block {
color: #999999;
}
.nav-tabs { .nav-tabs {
border: 0; border: 0;
} }
...@@ -167,7 +169,7 @@ footer a:hover { ...@@ -167,7 +169,7 @@ footer a:hover {
.page-header { .page-header {
border-bottom: none; border-bottom: none;
padding-bottom: 0px; padding-bottom: 0px;
margin-bottom: 20px; margin: 0;
} }
/* custom general page styles */ /* custom general page styles */
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -33,7 +33,11 @@ h2, h3 { ...@@ -33,7 +33,11 @@ h2, h3 {
} }
ul.breadcrumb { ul.breadcrumb {
margin: 80px 0 0 0; margin: 70px 0 0 0;
}
.breadcrumb li.active a {
color: #777;
} }
form select, form input, form textarea { form select, form input, form textarea {
...@@ -67,5 +71,4 @@ pre { ...@@ -67,5 +71,4 @@ pre {
.page-header { .page-header {
border-bottom: none; border-bottom: none;
padding-bottom: 0px; padding-bottom: 0px;
margin-bottom: 20px;
} }
{% 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,44 +26,42 @@ ...@@ -26,44 +26,42 @@
</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='navbar-brand' rel="nofollow" href='http://www.django-rest-framework.org'>
<a class='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 navbar-nav pull-right">
<ul class="nav pull-right"> {% block userlinks %}
{% block userlinks %} {% if user.is_authenticated %}
{% if user.is_authenticated %} {% optional_logout request user %}
{% optional_logout request user %} {% else %}
{% else %} {% optional_login request %}
{% optional_login request %} {% endif %}
{% endif %} {% endblock %}
{% endblock %} </ul>
</ul>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
<div class="container">
{% block breadcrumbs %} {% block breadcrumbs %}
<ul class="breadcrumb"> <ul class="breadcrumb">
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %} {% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
<li> {% if forloop.last %}
<a href="{{ breadcrumb_url }}" {% if forloop.last %}class="active"{% endif %}> <li class="active"><a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a></li>
{{ breadcrumb_name }} {% else %}
</a> <li><a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a></li>
{% if not forloop.last %}<span class="divider">&rsaquo;</span>{% endif %} {% endif %}
</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endblock %} {% endblock %}
...@@ -154,7 +152,7 @@ ...@@ -154,7 +152,7 @@
<div class="tab-pane" id="post-object-form"> <div class="tab-pane" id="post-object-form">
{% with form=post_form %} {% with form=post_form %}
<form action="{{ request.get_full_path }}" <form action="{{ request.get_full_path }}"
method="POST" enctype="multipart/form-data" class="form-horizontal"> method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
<fieldset> <fieldset>
{{ post_form }} {{ post_form }}
<div class="form-actions"> <div class="form-actions">
...@@ -199,7 +197,7 @@ ...@@ -199,7 +197,7 @@
{% if put_form %} {% if put_form %}
<div class="tab-pane" id="put-object-form"> <div class="tab-pane" id="put-object-form">
<form action="{{ request.get_full_path }}" <form action="{{ request.get_full_path }}"
method="POST" enctype="multipart/form-data" class="form-horizontal"> method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
<fieldset> <fieldset>
{{ put_form }} {{ put_form }}
<div class="form-actions"> <div class="form-actions">
...@@ -238,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>
<div class="form-group">
{% if field.label %}
<label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
{% endif %}
<div class="col-sm-10">
{% if style.inline %}
{% for key, text in field.choices.items %}
<label class="radio-inline">
<input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}>
{{ text }}
</label>
{% endfor %}
{% else %}
{% for key, text in field.choices.items %}
<div class="radio">
<label>
<input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}>
{{ text }}
</label>
</div>
{% endfor %}
{% endif %}
{% if field.errors %}
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
{% endif %}
{% if field.help_text %}
<span class="help-block">{{ field.help_text }}</span>
{% endif %}
</div>
</div>
<div class="form-group">
{% if field.label %}
<label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
{% endif %}
<div class="col-sm-10">
<select class="form-control" name="{{ field.name }}">
{% if field.allow_null %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %}
{% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
{% endfor %}
</select>
{% if field.errors %}
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
{% endif %}
{% if field.help_text %}
<span class="help-block">{{ field.help_text }}</span>
{% endif %}
</div>
</div>
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment