Merge pull request #1770 from tomchristie/2.4.0

2.4.0 Release.
This commit is contained in:
Tom Christie 2014-08-20 12:06:29 +01:00
commit 6ffc97c808
123 changed files with 1749 additions and 1845 deletions

View File

@ -12,11 +12,14 @@ env:
- DJANGO="django==1.6.5" - DJANGO="django==1.6.5"
- DJANGO="django==1.5.8" - DJANGO="django==1.5.8"
- DJANGO="django==1.4.13" - DJANGO="django==1.4.13"
- DJANGO="django==1.3.7"
install: install:
- pip install $DJANGO - pip install $DJANGO
- pip install defusedxml==0.3 Pillow==2.3.0 django-guardian==1.2.3 - pip install defusedxml==0.3
- pip install Pillow==2.3.0
- pip install django-guardian==1.2.3
- pip install pytest-django==2.6.1
- pip install flake8==2.2.2
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.4; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.4; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi"
@ -26,7 +29,7 @@ install:
- export PYTHONPATH=. - export PYTHONPATH=.
script: script:
- python rest_framework/runtests/runtests.py - ./runtests.py
matrix: matrix:
exclude: exclude:
@ -34,13 +37,7 @@ matrix:
env: DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/" env: DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/"
- python: "3.2" - python: "3.2"
env: DJANGO="django==1.4.13" env: DJANGO="django==1.4.13"
- python: "3.2"
env: DJANGO="django==1.3.7"
- python: "3.3" - python: "3.3"
env: DJANGO="django==1.4.13" env: DJANGO="django==1.4.13"
- python: "3.3"
env: DJANGO="django==1.3.7"
- python: "3.4" - python: "3.4"
env: DJANGO="django==1.4.13" env: DJANGO="django==1.4.13"
- python: "3.4"
env: DJANGO="django==1.3.7"

View File

@ -65,7 +65,7 @@ To run the tests, clone the repository, and then:
pip install -r optionals.txt pip install -r optionals.txt
# Run the tests # Run the tests
rest_framework/runtests/runtests.py py.test
You can also use the excellent [`tox`][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: You can also use the excellent [`tox`][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:

View File

@ -27,7 +27,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements # Requirements
* Python (2.6.5+, 2.7, 3.2, 3.3) * Python (2.6.5+, 2.7, 3.2, 3.3)
* Django (1.3, 1.4, 1.5, 1.6) * Django (1.4.2+, 1.5, 1.6, 1.7)
# Installation # Installation

View File

@ -126,7 +126,13 @@ To use the `TokenAuthentication` scheme you'll need to [configure the authentica
'rest_framework.authtoken' 'rest_framework.authtoken'
) )
Make sure to run `manage.py syncdb` after changing your settings. The `authtoken` database tables are managed by south (see [Schema migrations](#schema-migrations) below).
---
**Note:** Make sure to run `manage.py syncdb` after changing your settings. The `rest_framework.authtoken` app provides both Django (from v1.7) and South database migrations. See [Schema migrations](#schema-migrations) below.
---
You'll also need to create tokens for your users. You'll also need to create tokens for your users.
@ -198,7 +204,14 @@ Note that the default `obtain_auth_token` view explicitly uses JSON requests and
#### Schema migrations #### Schema migrations
The `rest_framework.authtoken` app includes a south migration that will create the authtoken table. The `rest_framework.authtoken` app includes both Django native migrations (for Django versions >1.7) and South migrations (for Django versions <1.7) that will create the authtoken table.
----
**Note**: From REST Framework v2.4.0 using South with Django <1.7 requires upgrading South v1.0+
----
If you're using a [custom user model][custom-user-model] you'll need to make sure that any initial migration that creates the user table runs before the authtoken table is created. If you're using a [custom user model][custom-user-model] you'll need to make sure that any initial migration that creates the user table runs before the authtoken table is created.

View File

@ -164,11 +164,12 @@ Corresponds to `django.db.models.fields.BooleanField`.
## CharField ## CharField
A text representation, optionally validates the text to be shorter than `max_length` and longer than `min_length`. A text representation, optionally validates the text to be shorter than `max_length` and longer than `min_length`.
If `allow_none` is `False` (default), `None` values will be converted to an empty string.
Corresponds to `django.db.models.fields.CharField` Corresponds to `django.db.models.fields.CharField`
or `django.db.models.fields.TextField`. or `django.db.models.fields.TextField`.
**Signature:** `CharField(max_length=None, min_length=None)` **Signature:** `CharField(max_length=None, min_length=None, allow_none=False)`
## URLField ## URLField

View File

@ -51,36 +51,41 @@ This means you'll need to explicitly set the `base_name` argument when registeri
### Extra link and actions ### Extra link and actions
Any methods on the viewset decorated with `@link` or `@action` will also be routed. Any methods on the viewset decorated with `@detail_route` or `@list_route` will also be routed.
For example, given a method like this on the `UserViewSet` class: 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 action from rest_framework.decorators import detail_route
@action(permission_classes=[IsAdminOrIsSelf]) class UserViewSet(ModelViewSet):
def set_password(self, request, pk=None):
... ...
@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None):
...
The following URL pattern would additionally be generated: The following URL pattern would additionally be generated:
* URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'` * URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'`
For more information see the viewset documentation on [marking extra actions for routing][route-decorators].
# API Guide # API Guide
## SimpleRouter ## SimpleRouter
This router includes routes for the standard set of `list`, `create`, `retrieve`, `update`, `partial_update` and `destroy` actions. The viewset can also mark additional methods to be routed, using the `@link` or `@action` decorators. This router includes routes for the standard set of `list`, `create`, `retrieve`, `update`, `partial_update` and `destroy` actions. The viewset can also mark additional methods to be routed, using the `@detail_route` or `@list_route` decorators.
<table border=1> <table border=1>
<tr><th>URL Style</th><th>HTTP Method</th><th>Action</th><th>URL Name</th></tr> <tr><th>URL Style</th><th>HTTP Method</th><th>Action</th><th>URL Name</th></tr>
<tr><td rowspan=2>{prefix}/</td><td>GET</td><td>list</td><td rowspan=2>{basename}-list</td></tr></tr> <tr><td rowspan=2>{prefix}/</td><td>GET</td><td>list</td><td rowspan=2>{basename}-list</td></tr></tr>
<tr><td>POST</td><td>create</td></tr> <tr><td>POST</td><td>create</td></tr>
<tr><td>{prefix}/{methodname}/</td><td>GET, or as specified by `methods` argument</td><td>`@list_route` decorated method</td><td>{basename}-{methodname}</td></tr>
<tr><td rowspan=4>{prefix}/{lookup}/</td><td>GET</td><td>retrieve</td><td rowspan=4>{basename}-detail</td></tr></tr> <tr><td rowspan=4>{prefix}/{lookup}/</td><td>GET</td><td>retrieve</td><td rowspan=4>{basename}-detail</td></tr></tr>
<tr><td>PUT</td><td>update</td></tr> <tr><td>PUT</td><td>update</td></tr>
<tr><td>PATCH</td><td>partial_update</td></tr> <tr><td>PATCH</td><td>partial_update</td></tr>
<tr><td>DELETE</td><td>destroy</td></tr> <tr><td>DELETE</td><td>destroy</td></tr>
<tr><td rowspan=2>{prefix}/{lookup}/{methodname}/</td><td>GET</td><td>@link decorated method</td><td rowspan=2>{basename}-{methodname}</td></tr> <tr><td>{prefix}/{lookup}/{methodname}/</td><td>GET, or as specified by `methods` argument</td><td>`@detail_route` decorated method</td><td>{basename}-{methodname}</td></tr>
<tr><td>POST</td><td>@action decorated method</td></tr>
</table> </table>
By default the URLs created by `SimpleRouter` are appended with a trailing slash. By default the URLs created by `SimpleRouter` are appended with a trailing slash.
@ -90,6 +95,12 @@ This behavior can be modified by setting the `trailing_slash` argument to `False
Trailing slashes are conventional in Django, but are not used by default in some other frameworks such as Rails. Which style you choose to use is largely a matter of preference, although some javascript frameworks may expect a particular routing style. Trailing slashes are conventional in Django, but are not used by default in some other frameworks such as Rails. Which style you choose to use is largely a matter of preference, although some javascript frameworks may expect a particular routing style.
The router will match lookup values containing any characters except slashes and period characters. For a more restrictive (or lenient) lookup pattern, set the `lookup_value_regex` attribute on the viewset. For example, you can limit the lookup to valid UUIDs:
class MyModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
lookup_field = 'my_model_id'
lookup_value_regex = '[0-9a-f]{32}'
## DefaultRouter ## DefaultRouter
This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes. This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes.
@ -99,12 +110,12 @@ This router is similar to `SimpleRouter` as above, but additionally includes a d
<tr><td>[.format]</td><td>GET</td><td>automatically generated root view</td><td>api-root</td></tr></tr> <tr><td>[.format]</td><td>GET</td><td>automatically generated root view</td><td>api-root</td></tr></tr>
<tr><td rowspan=2>{prefix}/[.format]</td><td>GET</td><td>list</td><td rowspan=2>{basename}-list</td></tr></tr> <tr><td rowspan=2>{prefix}/[.format]</td><td>GET</td><td>list</td><td rowspan=2>{basename}-list</td></tr></tr>
<tr><td>POST</td><td>create</td></tr> <tr><td>POST</td><td>create</td></tr>
<tr><td>{prefix}/{methodname}/[.format]</td><td>GET, or as specified by `methods` argument</td><td>`@list_route` decorated method</td><td>{basename}-{methodname}</td></tr>
<tr><td rowspan=4>{prefix}/{lookup}/[.format]</td><td>GET</td><td>retrieve</td><td rowspan=4>{basename}-detail</td></tr></tr> <tr><td rowspan=4>{prefix}/{lookup}/[.format]</td><td>GET</td><td>retrieve</td><td rowspan=4>{basename}-detail</td></tr></tr>
<tr><td>PUT</td><td>update</td></tr> <tr><td>PUT</td><td>update</td></tr>
<tr><td>PATCH</td><td>partial_update</td></tr> <tr><td>PATCH</td><td>partial_update</td></tr>
<tr><td>DELETE</td><td>destroy</td></tr> <tr><td>DELETE</td><td>destroy</td></tr>
<tr><td rowspan=2>{prefix}/{lookup}/{methodname}/[.format]</td><td>GET</td><td>@link decorated method</td><td rowspan=2>{basename}-{methodname}</td></tr> <tr><td>{prefix}/{lookup}/{methodname}/[.format]</td><td>GET, or as specified by `methods` argument</td><td>`@detail_route` decorated method</td><td>{basename}-{methodname}</td></tr>
<tr><td>POST</td><td>@action decorated method</td></tr>
</table> </table>
As with `SimpleRouter` the trailing slashes on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router. As with `SimpleRouter` the trailing slashes on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router.
@ -133,28 +144,87 @@ The arguments to the `Route` named tuple are:
**initkwargs**: A dictionary of any additional arguments that should be passed when instantiating the view. Note that the `suffix` argument is reserved for identifying the viewset type, used when generating the view name and breadcrumb links. **initkwargs**: A dictionary of any additional arguments that should be passed when instantiating the view. Note that the `suffix` argument is reserved for identifying the viewset type, used when generating the view name and breadcrumb links.
## Customizing dynamic routes
You can also customize how the `@list_route` and `@detail_route` decorators are routed.
To route either or both of these decorators, include a `DynamicListRoute` and/or `DynamicDetailRoute` named tuple in the `.routes` list.
The arguments to `DynamicListRoute` and `DynamicDetailRoute` are:
**url**: A string representing the URL to be routed. May include the same format strings as `Route`, and additionally accepts the `{methodname}` and `{methodnamehyphen}` format strings.
**name**: The name of the URL as used in `reverse` calls. May include the following format strings: `{basename}`, `{methodname}` and `{methodnamehyphen}`.
**initkwargs**: A dictionary of any additional arguments that should be passed when instantiating the view.
## Example ## Example
The following example will only route to the `list` and `retrieve` actions, and does not use the trailing slash convention. The following example will only route to the `list` and `retrieve` actions, and does not use the trailing slash convention.
from rest_framework.routers import Route, SimpleRouter from rest_framework.routers import Route, DynamicDetailRoute, SimpleRouter
class ReadOnlyRouter(SimpleRouter): class CustomReadOnlyRouter(SimpleRouter):
""" """
A router for read-only APIs, which doesn't use trailing slashes. A router for read-only APIs, which doesn't use trailing slashes.
""" """
routes = [ routes = [
Route(url=r'^{prefix}$', Route(
mapping={'get': 'list'}, url=r'^{prefix}$',
name='{basename}-list', mapping={'get': 'list'},
initkwargs={'suffix': 'List'}), name='{basename}-list',
Route(url=r'^{prefix}/{lookup}$', initkwargs={'suffix': 'List'}
mapping={'get': 'retrieve'}, ),
name='{basename}-detail', Route(
initkwargs={'suffix': 'Detail'}) url=r'^{prefix}/{lookup}$',
mapping={'get': 'retrieve'},
name='{basename}-detail',
initkwargs={'suffix': 'Detail'}
),
DynamicDetailRoute(
url=r'^{prefix}/{lookup}/{methodnamehyphen}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
)
] ]
The `SimpleRouter` class provides another example of setting the `.routes` attribute. Let's take a look at the routes our `CustomReadOnlyRouter` would generate for a simple viewset.
`views.py`:
class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
A viewset that provides the standard actions
"""
queryset = User.objects.all()
serializer_class = UserSerializer
lookup_field = 'username'
@detail_route()
def group_names(self, request):
"""
Returns a list of all the group names that the given
user belongs to.
"""
user = self.get_object()
groups = user.groups.all()
return Response([group.name for group in groups])
`urls.py`:
router = CustomReadOnlyRouter()
router.register('users', UserViewSet)
urlpatterns = router.urls
The following mappings would be generated...
<table border=1>
<tr><th>URL</th><th>HTTP Method</th><th>Action</th><th>URL Name</th></tr>
<tr><td>/users</td><td>GET</td><td>list</td><td>user-list</td></tr>
<tr><td>/users/{username}</td><td>GET</td><td>retrieve</td><td>user-detail</td></tr>
<tr><td>/users/{username}/group-names</td><td>GET</td><td>group_names</td><td>user-group-names</td></tr>
</table>
For another example of setting the `.routes` attribute, see the source code for the `SimpleRouter` class.
## Advanced custom routers ## Advanced custom routers
@ -184,6 +254,7 @@ The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (an
The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions-routers] for creating [nested viewsets][drf-extensions-nested-viewsets], [collection level controllers][drf-extensions-collection-level-controllers] with [customizable endpoint names][drf-extensions-customizable-endpoint-names]. The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions-routers] for creating [nested viewsets][drf-extensions-nested-viewsets], [collection level controllers][drf-extensions-collection-level-controllers] with [customizable endpoint names][drf-extensions-customizable-endpoint-names].
[cite]: http://guides.rubyonrails.org/routing.html [cite]: http://guides.rubyonrails.org/routing.html
[route-decorators]: viewsets.html#marking-extra-actions-for-routing
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
[wq.db]: http://wq.io/wq.db [wq.db]: http://wq.io/wq.db
[wq.db-router]: http://wq.io/docs/app.py [wq.db-router]: http://wq.io/docs/app.py
@ -191,4 +262,4 @@ The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions
[drf-extensions-routers]: http://chibisov.github.io/drf-extensions/docs/#routers [drf-extensions-routers]: http://chibisov.github.io/drf-extensions/docs/#routers
[drf-extensions-nested-viewsets]: http://chibisov.github.io/drf-extensions/docs/#nested-routes [drf-extensions-nested-viewsets]: http://chibisov.github.io/drf-extensions/docs/#nested-routes
[drf-extensions-collection-level-controllers]: http://chibisov.github.io/drf-extensions/docs/#collection-level-controllers [drf-extensions-collection-level-controllers]: http://chibisov.github.io/drf-extensions/docs/#collection-level-controllers
[drf-extensions-customizable-endpoint-names]: http://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name [drf-extensions-customizable-endpoint-names]: http://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name

View File

@ -377,5 +377,11 @@ The name of a parameter in the URL conf that may be used to provide a format suf
Default: `'format'` Default: `'format'`
#### NUM_PROXIES
An integer of 0 or more, that may be used to specify the number of application proxies that the API runs behind. This allows throttling to more accurately identify client IP addresses. If set to `None` then less strict IP matching will be used by the throttle classes.
Default: `None`
[cite]: http://www.python.org/dev/peps/pep-0020/ [cite]: http://www.python.org/dev/peps/pep-0020/
[strftime]: http://docs.python.org/2/library/time.html#time.strftime [strftime]: http://docs.python.org/2/library/time.html#time.strftime

View File

@ -35,7 +35,7 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C
'DEFAULT_THROTTLE_RATES': { 'DEFAULT_THROTTLE_RATES': {
'anon': '100/day', 'anon': '100/day',
'user': '1000/day' 'user': '1000/day'
} }
} }
The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `minute`, `hour` or `day` as the throttle period. The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `minute`, `hour` or `day` as the throttle period.
@ -66,6 +66,16 @@ Or, if you're using the `@api_view` decorator with function based views.
} }
return Response(content) return Response(content)
## How clients are identified
The `X-Forwarded-For` and `Remote-Addr` HTTP headers are used to uniquely identify client IP addresses for throttling. If the `X-Forwarded-For` header is present then it will be used, otherwise the value of the `Remote-Addr` header will be used.
If you need to strictly identify unique client IP addresses, you'll need to first configure the number of application proxies that the API runs behind by setting the `NUM_PROXIES` setting. This setting should be an integer of zero or more. If set to non-zero then the client IP will be identified as being the last IP address in the `X-Forwarded-For` header, once any application proxy IP addresses have first been excluded. If set to zero, then the `Remote-Addr` header will always be used as the identifying IP address.
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].
## Setting up the cache ## Setting up the cache
The throttle classes provided by REST framework use Django's cache backend. You should make sure that you've set appropriate [cache settings][cache-setting]. The default value of `LocMemCache` backend should be okay for simple setups. See Django's [cache documentation][cache-docs] for more details. The throttle classes provided by REST framework use Django's cache backend. You should make sure that you've set appropriate [cache settings][cache-setting]. The default value of `LocMemCache` backend should be okay for simple setups. See Django's [cache documentation][cache-docs] for more details.
@ -178,5 +188,6 @@ The following is an example of a rate throttle, that will randomly throttle 1 in
[cite]: https://dev.twitter.com/docs/error-codes-responses [cite]: https://dev.twitter.com/docs/error-codes-responses
[permissions]: permissions.md [permissions]: permissions.md
[identifing-clients]: http://oxpedia.org/wiki/index.php?title=AppSuite:Grizzly#Multiple_Proxies_in_front_of_the_cluster
[cache-setting]: https://docs.djangoproject.com/en/dev/ref/settings/#caches [cache-setting]: https://docs.djangoproject.com/en/dev/ref/settings/#caches
[cache-docs]: https://docs.djangoproject.com/en/dev/topics/cache/#setting-up-the-cache [cache-docs]: https://docs.djangoproject.com/en/dev/topics/cache/#setting-up-the-cache

View File

@ -70,7 +70,7 @@ There are two main advantages of using a `ViewSet` class over using a `View` cla
Both of these come with a trade-off. Using regular views and URL confs is more explicit and gives you more control. ViewSets are helpful if you want to get up and running quickly, or when you have a large API and you want to enforce a consistent URL configuration throughout. Both of these come with a trade-off. Using regular views and URL confs is more explicit and gives you more control. ViewSets are helpful if you want to get up and running quickly, or when you have a large API and you want to enforce a consistent URL configuration throughout.
## Marking extra methods for routing ## Marking extra actions for routing
The default routers included with REST framework will provide routes for a standard set of create/retrieve/update/destroy style operations, as shown below: The default routers included with REST framework will provide routes for a standard set of create/retrieve/update/destroy style operations, as shown below:
@ -101,14 +101,16 @@ The default routers included with REST framework will provide routes for a stand
def destroy(self, request, pk=None): def destroy(self, request, pk=None):
pass pass
If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@link` or `@action` decorators. The `@link` decorator will route `GET` requests, and the `@action` decorator will route `POST` requests. If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@detail_route` or `@list_route` decorators.
The `@detail_route` decorator contains `pk` in its URL pattern and is intended for methods which require a single instance. The `@list_route` decorator is intended for methods which operate on a list of objects.
For example: For example:
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework import viewsets
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework import viewsets
from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response from rest_framework.response import Response
from myapp.serializers import UserSerializer, PasswordSerializer from myapp.serializers import UserSerializer, PasswordSerializer
@ -119,7 +121,7 @@ For example:
queryset = User.objects.all() queryset = User.objects.all()
serializer_class = UserSerializer serializer_class = UserSerializer
@action() @detail_route(methods=['post'])
def set_password(self, request, pk=None): def set_password(self, request, pk=None):
user = self.get_object() user = self.get_object()
serializer = PasswordSerializer(data=request.DATA) serializer = PasswordSerializer(data=request.DATA)
@ -131,21 +133,27 @@ For example:
return Response(serializer.errors, return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST) status=status.HTTP_400_BAD_REQUEST)
The `@action` and `@link` decorators can additionally take extra arguments that will be set for the routed view only. For example... @list_route()
def recent_users(self, request):
recent_users = User.objects.all().order('-last_login')
page = self.paginate_queryset(recent_users)
serializer = self.get_pagination_serializer(page)
return Response(serializer.data)
@action(permission_classes=[IsAdminOrIsSelf]) The decorators can additionally take extra arguments that will be set for the routed view only. For example...
@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None): def set_password(self, request, pk=None):
... ...
The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example: Theses decorators will route `GET` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example:
@action(methods=['POST', 'DELETE']) @detail_route(methods=['post', 'delete'])
def unset_password(self, request, pk=None): def unset_password(self, request, pk=None):
... ...
The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$` The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$`
--- ---
# API Reference # API Reference

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@ -50,7 +50,7 @@ Some reasons you might want to use REST framework:
REST framework requires the following: REST framework requires the following:
* Python (2.6.5+, 2.7, 3.2, 3.3) * Python (2.6.5+, 2.7, 3.2, 3.3)
* Django (1.3, 1.4, 1.5, 1.6) * Django (1.4.2+, 1.5, 1.6, 1.7)
The following packages are optional: The following packages are optional:
@ -207,19 +207,9 @@ General guides to using REST framework.
## Development ## Development
If you want to work on REST framework itself, clone the repository, then... See the [Contribution guidelines][contributing] for information on how to clone
the repository, run the test suite and contribute changes back to REST
Build the docs: Framework.
./mkdocs.py
Run the tests:
./rest_framework/runtests/runtests.py
To run the tests against all supported configurations, first install [the tox testing tool][tox] globally, using `pip install tox`, then simply run `tox`:
tox
## Support ## Support

View File

@ -0,0 +1,147 @@
# REST framework 2.4 announcement
The 2.4 release is largely an intermediate step, tying up some outstanding issues prior to the 3.x series.
## Version requirements
Support for Django 1.3 has been dropped.
The lowest supported version of Django is now 1.4.2.
The current plan is for REST framework to remain in lockstep with [Django's long-term support policy][lts-releases].
## Django 1.7 support
The optional authtoken application now includes support for *both* Django 1.7 schema migrations, *and* for old-style `south` migrations.
**If you are using authtoken, and you want to continue using `south`, you must upgrade your `south` package to version 1.0.**
## Updated test runner
We now have a new test runner for developing against the project,, that uses the excellent [py.test](http://pytest.org) library.
To use it make sure you have first installed the test requirements.
pip install -r requirements-test.txt
Then run the `runtests.py` script.
./runtests.py
The new test runner also includes [flake8](https://flake8.readthedocs.org) code linting, which should help keep our coding style consistent.
#### Test runner flags
Run using a more concise output style.
./runtests -q
Run the tests using a more concise output style, no coverage, no flake8.
./runtests --fast
Don't run the flake8 code linting.
./runtests --nolint
Only run the flake8 code linting, don't run the tests.
./runtests --lintonly
Run the tests for a given test case.
./runtests MyTestCase
Run the tests for a given test method.
./runtests MyTestCase.test_this_method
Shorter form to run the tests for a given test method.
./runtests 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.
## Improved viewset routing
The `@action` and `@link` decorators were inflexible in that they only allowed additional routes to be added against instance style URLs, not against list style URLs.
The `@action` and `@link` decorators have now been moved to pending deprecation, and the `@list_route` and `@detail_route` decroators have been introduced.
Here's an example of using the new decorators. Firstly we have a detail-type route named "set_password" that acts on a single instance, and takes a `pk` argument in the URL. Secondly we have a list-type route named "recent_users" that acts on a queryset, and does not take any arguments in the URL.
class UserViewSet(viewsets.ModelViewSet):
"""
A viewset that provides the standard actions
"""
queryset = User.objects.all()
serializer_class = UserSerializer
@detail_route(methods=['post'])
def set_password(self, request, pk=None):
user = self.get_object()
serializer = PasswordSerializer(data=request.DATA)
if serializer.is_valid():
user.set_password(serializer.data['password'])
user.save()
return Response({'status': 'password set'})
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
@list_route()
def recent_users(self, request):
recent_users = User.objects.all().order('-last_login')
page = self.paginate_queryset(recent_users)
serializer = self.get_pagination_serializer(page)
return Response(serializer.data)
For more details, see the [viewsets documentation](../api-guide/viewsets.md).
## Other features
There are also a number of other features and bugfixes as [listed in the release notes][2-4-release-notes]. In particular these include:
[Customizable view name and description functions][view-name-and-description-settings] for use with the browsable API, by using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings.
Smarter [client IP identification for throttling][client-ip-identification], with the addition of the `NUM_PROXIES` setting.
## Deprecations
All API changes in 2.3 that previously raised `PendingDeprecationWarning` will now raise a `DeprecationWarning`, which is loud by default.
All API changes in 2.3 that previously raised `DeprecationWarning` have now been removed entirely.
Furter details on these deprecations is available in the [2.3 announcement][2-3-announcement].
## Labels and milestones
Although not strictly part of the 2.4 release it's also worth noting here that we've been working hard towards improving our triage process.
The [labels that we use in GitHub][github-labels] have been cleaned up, and all existing tickets triaged. Any given ticket should have one and only one label, indicating its current state.
We've also [started using milestones][github-milestones] in order to track tickets against particular releases.
---
![Labels and milestones](../img/labels-and-milestones.png)
**Above**: *Overview of our current use of labels and milestones in GitHub.*
---
We hope both of these changes will help make the management process more clear and obvious and help keep tickets well-organised and relevant.
## Next steps
The next planned release will be 3.0, featuring an improved and simplified serializer implementation.
Once again, many thanks to all the generous [backers and sponsors][kickstarter-sponsors] who've helped make this possible!
[lts-releases]: https://docs.djangoproject.com/en/dev/internals/release-process/#long-term-support-lts-releases
[2-4-release-notes]: ./topics/release-notes/#240
[view-name-and-description-settings]: ../api-guide/settings/#view-names-and-descriptions
[client-ip-identification]: ../api-guide/throttling/#how-clients-are-identified
[2-3-announcement]: ./topics/2.3-announcement
[github-labels]: https://github.com/tomchristie/django-rest-framework/issues
[github-milestones]: https://github.com/tomchristie/django-rest-framework/milestones
[kickstarter-sponsors]: ./topics/kickstarter-announcement/#sponsors

View File

@ -62,10 +62,44 @@ To run the tests, clone the repository, and then:
virtualenv env virtualenv env
source env/bin/activate source env/bin/activate
pip install -r requirements.txt pip install -r requirements.txt
pip install -r optionals.txt pip install -r requirements-test.txt
# Run the tests # Run the tests
rest_framework/runtests/runtests.py ./runtests.py
### Test options
Run using a more concise output style.
./runtests -q
Run the tests using a more concise output style, no coverage, no flake8.
./runtests --fast
Don't run the flake8 code linting.
./runtests --nolint
Only run the flake8 code linting, don't run the tests.
./runtests --lintonly
Run the tests for a given test case.
./runtests MyTestCase
Run the tests for a given test method.
./runtests MyTestCase.test_this_method
Shorter form to run the tests for a given test method.
./runtests 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.
### Running against multiple environments
You can also use the excellent [tox][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: You can also use the excellent [tox][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:

View File

@ -38,6 +38,33 @@ You can determine your currently installed version using `pip freeze`:
--- ---
## 2.4.x series
### 2.4.0
**Django version requirements**: The lowest supported version of Django is now 1.4.2.
**South version requirements**: This note applies to any users using the optional `authtoken` application, which includes an associated database migration. You must now *either* upgrade your `south` package to version 1.0, *or* instead use the built-in migration support available with Django 1.7.
* Added compatibility with Django 1.7's database migration support.
* New test runner, using `py.test`.
* `@detail_route` and `@list_route` decorators replace `@action` and `@link`.
* Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings.
* Added `NUM_PROXIES` setting for smarter client IP identification.
* Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute.
* Added `cache` attribute to throttles to allow overriding of default cache.
* Added `lookup_value_regex` attribute to routers, to allow the URL argument matching to be constrainted by the user.
* Added `allow_none` option to `CharField`.
* Support Django's standard `status_code` class attribute on responses.
* More intuitive behavior on the test client, as `client.logout()` now also removes any credentials that have been set.
* Bugfix: `?page_size=0` query parameter now falls back to default page size for view, instead of always turning pagination off.
* Bugfix: Always uppercase `X-Http-Method-Override` methods.
* Bugfix: Copy `filter_backends` list before returning it, in order to prevent view code from mutating the class attribute itself.
* Bugfix: Set the `.action` attribute on viewsets when introspected by `OPTIONS` for testing permissions on the view.
* Bugfix: Ensure `ValueError` raised during deserialization results in a error list rather than a single error. This is now consistent with other validation errors.
---
## 2.3.x series ## 2.3.x series
### 2.3.14 ### 2.3.14
@ -169,9 +196,9 @@ You can determine your currently installed version using `pip freeze`:
* Added `trailing_slash` option to routers. * Added `trailing_slash` option to routers.
* Include support for `HttpStreamingResponse`. * Include support for `HttpStreamingResponse`.
* Support wider range of default serializer validation when used with custom model fields. * Support wider range of default serializer validation when used with custom model fields.
* UTF-8 Support for browsable API descriptions. * UTF-8 Support for browsable API descriptions.
* OAuth2 provider uses timezone aware datetimes when supported. * OAuth2 provider uses timezone aware datetimes when supported.
* Bugfix: Return error correctly when OAuth non-existent consumer occurs. * Bugfix: Return error correctly when OAuth non-existent consumer occurs.
* Bugfix: Allow `FileUploadParser` to correctly filename if provided as URL kwarg. * Bugfix: Allow `FileUploadParser` to correctly filename if provided as URL kwarg.
* Bugfix: Fix `ScopedRateThrottle`. * Bugfix: Fix `ScopedRateThrottle`.
@ -212,7 +239,7 @@ You can determine your currently installed version using `pip freeze`:
* Added SearchFilter * Added SearchFilter
* Added OrderingFilter * Added OrderingFilter
* Added GenericViewSet * Added GenericViewSet
* Bugfix: Multiple `@action` and `@link` methods now allowed on viewsets. * Bugfix: Multiple `@action` and `@link` methods now allowed on viewsets.
* Bugfix: Fix API Root view issue with DjangoModelPermissions * Bugfix: Fix API Root view issue with DjangoModelPermissions
### 2.3.2 ### 2.3.2
@ -265,7 +292,7 @@ You can determine your currently installed version using `pip freeze`:
* Long HTTP headers in browsable API are broken in multiple lines when possible. * Long HTTP headers in browsable API are broken in multiple lines when possible.
* Bugfix: Fix regression with DjangoFilterBackend not worthing correctly with single object views. * Bugfix: Fix regression with DjangoFilterBackend not worthing correctly with single object views.
* Bugfix: OAuth should fail hard when invalid token used. * Bugfix: OAuth should fail hard when invalid token used.
* Bugfix: Fix serializer potentially returning `None` object for models that define `__bool__` or `__len__`. * Bugfix: Fix serializer potentially returning `None` object for models that define `__bool__` or `__len__`.
### 2.2.5 ### 2.2.5

View File

@ -25,7 +25,7 @@ Here we've used the `ReadOnlyModelViewSet` class to automatically provide the de
Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class. Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class.
from rest_framework.decorators import link from rest_framework.decorators import detail_route
class SnippetViewSet(viewsets.ModelViewSet): class SnippetViewSet(viewsets.ModelViewSet):
""" """
@ -39,7 +39,7 @@ Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighl
permission_classes = (permissions.IsAuthenticatedOrReadOnly, permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,) IsOwnerOrReadOnly,)
@link(renderer_classes=[renderers.StaticHTMLRenderer]) @detail_route(renderer_classes=[renderers.StaticHTMLRenderer])
def highlight(self, request, *args, **kwargs): def highlight(self, request, *args, **kwargs):
snippet = self.get_object() snippet = self.get_object()
return Response(snippet.highlighted) return Response(snippet.highlighted)
@ -49,9 +49,9 @@ Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighl
This time we've used the `ModelViewSet` class in order to get the complete set of default read and write operations. This time we've used the `ModelViewSet` class in order to get the complete set of default read and write operations.
Notice that we've also used the `@link` decorator to create a custom action, named `highlight`. This decorator can be used to add any custom endpoints that don't fit into the standard `create`/`update`/`delete` style. Notice that we've also used the `@detail_route` decorator to create a custom action, named `highlight`. This decorator can be used to add any custom endpoints that don't fit into the standard `create`/`update`/`delete` style.
Custom actions which use the `@link` decorator will respond to `GET` requests. We could have instead used the `@action` decorator if we wanted an action that responded to `POST` requests. Custom actions which use the `@detail_route` decorator will respond to `GET` requests. We can use the `methods` argument if we wanted an action that responded to `POST` requests.
## Binding ViewSets to URLs explicitly ## Binding ViewSets to URLs explicitly

View File

@ -1,3 +1,10 @@
# Test requirements
pytest-django==2.6
pytest==2.5.2
pytest-cov==1.6
flake8==2.2.2
# Optional packages
markdown>=2.1.0 markdown>=2.1.0
PyYAML>=3.10 PyYAML>=3.10
defusedxml>=0.3 defusedxml>=0.3

View File

@ -1,9 +1,9 @@
""" """
______ _____ _____ _____ __ _ ______ _____ _____ _____ __
| ___ \ ___/ ___|_ _| / _| | | | ___ \ ___/ ___|_ _| / _| | |
| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| | __ | |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| |__
| /| __| `--. \ | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ / | /| __| `--. \ | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ /
| |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | < | |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | <
\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_| \_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_|
""" """

View File

@ -6,9 +6,9 @@ import base64
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.middleware.csrf import CsrfViewMiddleware
from django.conf import settings from django.conf import settings
from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework import exceptions, HTTP_HEADER_ENCODING
from rest_framework.compat import CsrfViewMiddleware
from rest_framework.compat import oauth, oauth_provider, oauth_provider_store from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
from rest_framework.compat import oauth2_provider, provider_now, check_nonce from rest_framework.compat import oauth2_provider, provider_now, check_nonce
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
@ -21,7 +21,7 @@ def get_authorization_header(request):
Hide some test client ickyness where the header can be unicode. Hide some test client ickyness where the header can be unicode.
""" """
auth = request.META.get('HTTP_AUTHORIZATION', b'') auth = request.META.get('HTTP_AUTHORIZATION', b'')
if type(auth) == type(''): if isinstance(auth, type('')):
# Work around django test client oddness # Work around django test client oddness
auth = auth.encode(HTTP_HEADER_ENCODING) auth = auth.encode(HTTP_HEADER_ENCODING)
return auth return auth

View File

@ -1,67 +1,27 @@
# -*- coding: utf-8 -*- # encoding: utf8
import datetime from __future__ import unicode_literals
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
from rest_framework.settings import api_settings from django.db import models, migrations
from django.conf import settings
try: class Migration(migrations.Migration):
from django.contrib.auth import get_user_model
except ImportError: # django < 1.5
from django.contrib.auth.models import User
else:
User = get_user_model()
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
class Migration(SchemaMigration): operations = [
migrations.CreateModel(
def forwards(self, orm): name='Token',
# Adding model 'Token' fields=[
db.create_table('authtoken_token', ( ('key', models.CharField(max_length=40, serialize=False, primary_key=True)),
('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)), ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, to_field='id')),
('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])), ('created', models.DateTimeField(auto_now_add=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), ],
)) options={
db.send_create_signal('authtoken', ['Token']) 'abstract': False,
},
bases=(models.Model,),
def backwards(self, orm): ),
# Deleting model 'Token' ]
db.delete_table('authtoken_token')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
"%s.%s" % (User._meta.app_label, User._meta.module_name): {
'Meta': {'object_name': User._meta.module_name},
},
'authtoken.token': {
'Meta': {'object_name': 'Token'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['authtoken']

View File

@ -1,6 +1,5 @@
import binascii import binascii
import os import os
from hashlib import sha1
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models

View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
from south.db import db
from south.v2 import SchemaMigration
try:
from django.contrib.auth import get_user_model
except ImportError: # django < 1.5
from django.contrib.auth.models import User
else:
User = get_user_model()
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Token'
db.create_table('authtoken_token', (
('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
))
db.send_create_signal('authtoken', ['Token'])
def backwards(self, orm):
# Deleting model 'Token'
db.delete_table('authtoken_token')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
"%s.%s" % (User._meta.app_label, User._meta.module_name): {
'Meta': {'object_name': User._meta.module_name},
},
'authtoken.token': {
'Meta': {'object_name': 'Token'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['authtoken']

View File

@ -5,25 +5,14 @@ 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 django
import inspect 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
# Try to import six from Django, fallback to included `six`.
try:
from django.utils import six
except ImportError:
from rest_framework import six
# location of patterns, url, include changes in 1.4 onwards # Handle django.utils.encoding rename in 1.5 onwards.
try:
from django.conf.urls import patterns, url, include
except ImportError:
from django.conf.urls.defaults import patterns, url, include
# Handle django.utils.encoding rename:
# smart_unicode -> smart_text # smart_unicode -> smart_text
# force_unicode -> force_text # force_unicode -> force_text
try: try:
@ -42,12 +31,14 @@ try:
except ImportError: except ImportError:
from django.http import HttpResponse as HttpResponseBase from django.http import HttpResponse as HttpResponseBase
# django-filter is optional # django-filter is optional
try: try:
import django_filters import django_filters
except ImportError: except ImportError:
django_filters = None django_filters = None
# 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
@ -108,46 +99,13 @@ def get_concrete_model(model_cls):
return model_cls return model_cls
# View._allowed_methods only present from 1.5 onwards
if django.VERSION >= (1, 5): if django.VERSION >= (1, 5):
from django.views.generic import View from django.views.generic import View
else: else:
from django.views.generic import View as _View from django.views.generic import View as DjangoView
from django.utils.decorators import classonlymethod
from django.utils.functional import update_wrapper
class View(_View): class View(DjangoView):
# 1.3 does not include head method in base View class
# See: https://code.djangoproject.com/ticket/15668
@classonlymethod
def as_view(cls, **initkwargs):
"""
Main entry point for a request-response process.
"""
# sanitize keyword arguments
for key in initkwargs:
if key in cls.http_method_names:
raise TypeError("You tried to pass in the %s method name as a "
"keyword argument to %s(). Don't do that."
% (key, cls.__name__))
if not hasattr(cls, key):
raise TypeError("%s() received an invalid keyword %r" % (
cls.__name__, key))
def view(request, *args, **kwargs):
self = cls(**initkwargs)
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
return self.dispatch(request, *args, **kwargs)
# take name and docstring from class
update_wrapper(view, cls, updated=())
# and possible attributes set by decorators
# like csrf_exempt from dispatch
update_wrapper(view, cls.dispatch, assigned=())
return view
# _allowed_methods only present from 1.5 onwards
def _allowed_methods(self): def _allowed_methods(self):
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)]
@ -157,316 +115,16 @@ 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']
# PUT, DELETE do not require CSRF until 1.4. They should. Make it better. # RequestFactory only provides `generic` from 1.5 onwards
if django.VERSION >= (1, 4):
from django.middleware.csrf import CsrfViewMiddleware
else:
import hashlib
import re
import random
import logging
from django.conf import settings
from django.core.urlresolvers import get_callable
try:
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass
logger = logging.getLogger('django.request')
if not logger.handlers:
logger.addHandler(NullHandler())
def same_origin(url1, url2):
"""
Checks if two URLs are 'same-origin'
"""
p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2)
return p1[0:2] == p2[0:2]
def constant_time_compare(val1, val2):
"""
Returns True if the two strings are equal, False otherwise.
The time taken is independent of the number of characters that match.
"""
if len(val1) != len(val2):
return False
result = 0
for x, y in zip(val1, val2):
result |= ord(x) ^ ord(y)
return result == 0
# Use the system (hardware-based) random number generator if it exists.
if hasattr(random, 'SystemRandom'):
randrange = random.SystemRandom().randrange
else:
randrange = random.randrange
_MAX_CSRF_KEY = 18446744073709551616 # 2 << 63
REASON_NO_REFERER = "Referer checking failed - no Referer."
REASON_BAD_REFERER = "Referer checking failed - %s does not match %s."
REASON_NO_CSRF_COOKIE = "CSRF cookie not set."
REASON_BAD_TOKEN = "CSRF token missing or incorrect."
def _get_failure_view():
"""
Returns the view to be used for CSRF rejections
"""
return get_callable(settings.CSRF_FAILURE_VIEW)
def _get_new_csrf_key():
return hashlib.md5("%s%s" % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest()
def get_token(request):
"""
Returns the the CSRF token required for a POST form. The token is an
alphanumeric value.
A side effect of calling this function is to make the the csrf_protect
decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie'
header to the outgoing response. For this reason, you may need to use this
function lazily, as is done by the csrf context processor.
"""
request.META["CSRF_COOKIE_USED"] = True
return request.META.get("CSRF_COOKIE", None)
def _sanitize_token(token):
# Allow only alphanum, and ensure we return a 'str' for the sake of the post
# processing middleware.
token = re.sub('[^a-zA-Z0-9]', '', str(token.decode('ascii', 'ignore')))
if token == "":
# In case the cookie has been truncated to nothing at some point.
return _get_new_csrf_key()
else:
return token
class CsrfViewMiddleware(object):
"""
Middleware that requires a present and correct csrfmiddlewaretoken
for POST requests that have a CSRF cookie, and sets an outgoing
CSRF cookie.
This middleware should be used in conjunction with the csrf_token template
tag.
"""
# The _accept and _reject methods currently only exist for the sake of the
# requires_csrf_token decorator.
def _accept(self, request):
# Avoid checking the request twice by adding a custom attribute to
# request. This will be relevant when both decorator and middleware
# are used.
request.csrf_processing_done = True
return None
def _reject(self, request, reason):
return _get_failure_view()(request, reason=reason)
def process_view(self, request, callback, callback_args, callback_kwargs):
if getattr(request, 'csrf_processing_done', False):
return None
try:
csrf_token = _sanitize_token(request.COOKIES[settings.CSRF_COOKIE_NAME])
# Use same token next time
request.META['CSRF_COOKIE'] = csrf_token
except KeyError:
csrf_token = None
# Generate token and store it in the request, so it's available to the view.
request.META["CSRF_COOKIE"] = _get_new_csrf_key()
# Wait until request.META["CSRF_COOKIE"] has been manipulated before
# bailing out, so that get_token still works
if getattr(callback, 'csrf_exempt', False):
return None
# Assume that anything not defined as 'safe' by RC2616 needs protection.
if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
if getattr(request, '_dont_enforce_csrf_checks', False):
# Mechanism to turn off CSRF checks for test suite. It comes after
# the creation of CSRF cookies, so that everything else continues to
# work exactly the same (e.g. cookies are sent etc), but before the
# any branches that call reject()
return self._accept(request)
if request.is_secure():
# Suppose user visits http://example.com/
# An active network attacker,(man-in-the-middle, MITM) sends a
# POST form which targets https://example.com/detonate-bomb/ and
# submits it via javascript.
#
# The attacker will need to provide a CSRF cookie and token, but
# that is no problem for a MITM and the session independent
# nonce we are using. So the MITM can circumvent the CSRF
# protection. This is true for any HTTP connection, but anyone
# using HTTPS expects better! For this reason, for
# https://example.com/ we need additional protection that treats
# http://example.com/ as completely untrusted. Under HTTPS,
# Barth et al. found that the Referer header is missing for
# same-domain requests in only about 0.2% of cases or less, so
# we can use strict Referer checking.
referer = request.META.get('HTTP_REFERER')
if referer is None:
logger.warning('Forbidden (%s): %s' % (REASON_NO_REFERER, request.path),
extra={
'status_code': 403,
'request': request,
}
)
return self._reject(request, REASON_NO_REFERER)
# Note that request.get_host() includes the port
good_referer = 'https://%s/' % request.get_host()
if not same_origin(referer, good_referer):
reason = REASON_BAD_REFERER % (referer, good_referer)
logger.warning('Forbidden (%s): %s' % (reason, request.path),
extra={
'status_code': 403,
'request': request,
}
)
return self._reject(request, reason)
if csrf_token is None:
# No CSRF cookie. For POST requests, we insist on a CSRF cookie,
# and in this way we can avoid all CSRF attacks, including login
# CSRF.
logger.warning('Forbidden (%s): %s' % (REASON_NO_CSRF_COOKIE, request.path),
extra={
'status_code': 403,
'request': request,
}
)
return self._reject(request, REASON_NO_CSRF_COOKIE)
# check non-cookie token for match
request_csrf_token = ""
if request.method == "POST":
request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
if request_csrf_token == "":
# Fall back to X-CSRFToken, to make things easier for AJAX,
# and possible for PUT/DELETE
request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '')
if not constant_time_compare(request_csrf_token, csrf_token):
logger.warning('Forbidden (%s): %s' % (REASON_BAD_TOKEN, request.path),
extra={
'status_code': 403,
'request': request,
}
)
return self._reject(request, REASON_BAD_TOKEN)
return self._accept(request)
# timezone support is new in Django 1.4
try:
from django.utils import timezone
except ImportError:
timezone = None
# dateparse is ALSO new in Django 1.4
try:
from django.utils.dateparse import parse_date, parse_datetime, parse_time
except ImportError:
import datetime
import re
date_re = re.compile(
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$'
)
datetime_re = re.compile(
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
r'(?P<tzinfo>Z|[+-]\d{1,2}:\d{1,2})?$'
)
time_re = re.compile(
r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
)
def parse_date(value):
match = date_re.match(value)
if match:
kw = dict((k, int(v)) for k, v in match.groupdict().iteritems())
return datetime.date(**kw)
def parse_time(value):
match = time_re.match(value)
if match:
kw = match.groupdict()
if kw['microsecond']:
kw['microsecond'] = kw['microsecond'].ljust(6, '0')
kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
return datetime.time(**kw)
def parse_datetime(value):
"""Parse datetime, but w/o the timezone awareness in 1.4"""
match = datetime_re.match(value)
if match:
kw = match.groupdict()
if kw['microsecond']:
kw['microsecond'] = kw['microsecond'].ljust(6, '0')
kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
return datetime.datetime(**kw)
# smart_urlquote is new on Django 1.4
try:
from django.utils.html import smart_urlquote
except ImportError:
import re
from django.utils.encoding import smart_str
try:
from urllib.parse import quote, urlsplit, urlunsplit
except ImportError: # Python 2
from urllib import quote
from urlparse import urlsplit, urlunsplit
unquoted_percents_re = re.compile(r'%(?![0-9A-Fa-f]{2})')
def smart_urlquote(url):
"Quotes a URL if it isn't already quoted."
# Handle IDN before quoting.
scheme, netloc, path, query, fragment = urlsplit(url)
try:
netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE
except UnicodeError: # invalid domain part
pass
else:
url = urlunsplit((scheme, netloc, path, query, fragment))
# An URL is considered unquoted if it contains no % characters or
# contains a % not followed by two hexadecimal digits. See #9655.
if '%' not in url or unquoted_percents_re.search(url):
# See http://bugs.python.org/issue2637
url = quote(smart_str(url), safe=b'!*\'();:@&=+$,/?#[]~')
return force_text(url)
# RequestFactory only provide `generic` from 1.5 onwards
from django.test.client import RequestFactory as DjangoRequestFactory from django.test.client import RequestFactory as DjangoRequestFactory
from django.test.client import FakePayload from django.test.client import FakePayload
try: try:
# In 1.5 the test client uses force_bytes # In 1.5 the test client uses force_bytes
from django.utils.encoding import force_bytes as force_bytes_or_smart_bytes from django.utils.encoding import force_bytes as force_bytes_or_smart_bytes
except ImportError: except ImportError:
# In 1.3 and 1.4 the test client just uses smart_str # In 1.4 the test client just uses smart_str
from django.utils.encoding import smart_str as force_bytes_or_smart_bytes from django.utils.encoding import smart_str as force_bytes_or_smart_bytes
class RequestFactory(DjangoRequestFactory): class RequestFactory(DjangoRequestFactory):
def generic(self, method, path, def generic(self, method, path,
data='', content_type='application/octet-stream', **extra): data='', content_type='application/octet-stream', **extra):
@ -491,6 +149,7 @@ class RequestFactory(DjangoRequestFactory):
r.update(extra) r.update(extra)
return self.request(**r) return self.request(**r)
# Markdown is optional # Markdown is optional
try: try:
import markdown import markdown
@ -505,7 +164,6 @@ try:
safe_mode = False safe_mode = False
md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode) md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode)
return md.convert(text) return md.convert(text)
except ImportError: except ImportError:
apply_markdown = None apply_markdown = None
@ -523,14 +181,16 @@ try:
except ImportError: except ImportError:
etree = None etree = None
# OAuth is optional
# OAuth2 is optional
try: try:
# Note: The `oauth2` package actually provides oauth1.0a support. Urg. # Note: The `oauth2` package actually provides oauth1.0a support. Urg.
import oauth2 as oauth import oauth2 as oauth
except ImportError: except ImportError:
oauth = None oauth = None
# OAuth is optional
# OAuthProvider is optional
try: try:
import oauth_provider import oauth_provider
from oauth_provider.store import store as oauth_provider_store from oauth_provider.store import store as oauth_provider_store
@ -552,6 +212,7 @@ except (ImportError, ImproperlyConfigured):
oauth_provider_store = None oauth_provider_store = None
check_nonce = None check_nonce = None
# OAuth 2 support is optional # OAuth 2 support is optional
try: try:
import provider as oauth2_provider import provider as oauth2_provider
@ -571,7 +232,8 @@ except ImportError:
oauth2_constants = None oauth2_constants = None
provider_now = None provider_now = None
# Handle lazy strings
# Handle lazy strings across Py2/Py3
from django.utils.functional import Promise from django.utils.functional import Promise
if six.PY3: if six.PY3:

View File

@ -3,13 +3,14 @@ The most important decorator in this module is `@api_view`, which is used
for writing function-based views with REST framework. for writing function-based views with REST framework.
There are also various decorators for setting the API policies on function There are also various decorators for setting the API policies on function
based views, as well as the `@action` and `@link` decorators, which are based views, as well as the `@detail_route` and `@list_route` decorators, which are
used to annotate methods on viewsets that should be included by routers. used to annotate methods on viewsets that should be included by routers.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework.compat 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):
@ -107,23 +108,59 @@ def permission_classes(permission_classes):
return decorator return decorator
def link(**kwargs): def detail_route(methods=['get'], **kwargs):
""" """
Used to mark a method on a ViewSet that should be routed for GET requests. Used to mark a method on a ViewSet that should be routed for detail requests.
""" """
def decorator(func): def decorator(func):
func.bind_to_methods = ['get'] func.bind_to_methods = methods
func.detail = True
func.kwargs = kwargs func.kwargs = kwargs
return func return func
return decorator return decorator
def list_route(methods=['get'], **kwargs):
"""
Used to mark a method on a ViewSet that should be routed for list requests.
"""
def decorator(func):
func.bind_to_methods = methods
func.detail = False
func.kwargs = kwargs
return func
return decorator
# These are now pending deprecation, in favor of `detail_route` and `list_route`.
def link(**kwargs):
"""
Used to mark a method on a ViewSet that should be routed for detail GET requests.
"""
msg = 'link is pending deprecation. Use detail_route instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
def decorator(func):
func.bind_to_methods = ['get']
func.detail = True
func.kwargs = kwargs
return func
return decorator
def action(methods=['post'], **kwargs): def action(methods=['post'], **kwargs):
""" """
Used to mark a method on a ViewSet that should be routed for POST requests. 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): def decorator(func):
func.bind_to_methods = methods func.bind_to_methods = methods
func.detail = True
func.kwargs = kwargs func.kwargs = kwargs
return func return func
return decorator return decorator

View File

@ -23,6 +23,7 @@ class APIException(Exception):
def __str__(self): def __str__(self):
return self.detail return 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.'

View File

@ -18,12 +18,14 @@ from django.conf import settings
from django.db.models.fields import BLANK_CHOICE_DASH from django.db.models.fields import BLANK_CHOICE_DASH
from django.http import QueryDict from django.http import QueryDict
from django.forms import widgets from django.forms import widgets
from django.utils import six, timezone
from django.utils.encoding import is_protected_type from django.utils.encoding import is_protected_type
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from django.utils.dateparse import parse_date, parse_datetime, parse_time
from rest_framework import ISO_8601 from rest_framework import ISO_8601
from rest_framework.compat import ( from rest_framework.compat import (
timezone, parse_date, parse_datetime, parse_time, BytesIO, six, smart_text, BytesIO, smart_text,
force_text, is_non_str_iterable force_text, is_non_str_iterable
) )
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -61,8 +63,10 @@ def get_component(obj, attr_name):
def readable_datetime_formats(formats): def readable_datetime_formats(formats):
format = ', '.join(formats).replace(ISO_8601, format = ', '.join(formats).replace(
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]') ISO_8601,
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'
)
return humanize_strptime(format) return humanize_strptime(format)
@ -265,13 +269,6 @@ class WritableField(Field):
validators=[], error_messages=None, widget=None, validators=[], error_messages=None, widget=None,
default=None, blank=None): default=None, blank=None):
# 'blank' is to be deprecated in favor of 'required'
if blank is not None:
warnings.warn('The `blank` keyword argument is deprecated. '
'Use the `required` keyword argument instead.',
DeprecationWarning, stacklevel=2)
required = not(blank)
super(WritableField, self).__init__(source=source, label=label, help_text=help_text) super(WritableField, self).__init__(source=source, label=label, help_text=help_text)
self.read_only = read_only self.read_only = read_only
@ -430,7 +427,7 @@ class ModelField(WritableField):
} }
##### Typed Fields ##### # Typed Fields
class BooleanField(WritableField): class BooleanField(WritableField):
type_name = 'BooleanField' type_name = 'BooleanField'
@ -465,8 +462,9 @@ class CharField(WritableField):
type_label = 'string' type_label = 'string'
form_field_class = forms.CharField form_field_class = forms.CharField
def __init__(self, max_length=None, min_length=None, *args, **kwargs): def __init__(self, max_length=None, min_length=None, allow_none=False, *args, **kwargs):
self.max_length, self.min_length = max_length, min_length self.max_length, self.min_length = max_length, min_length
self.allow_none = allow_none
super(CharField, self).__init__(*args, **kwargs) super(CharField, self).__init__(*args, **kwargs)
if min_length is not None: if min_length is not None:
self.validators.append(validators.MinLengthValidator(min_length)) self.validators.append(validators.MinLengthValidator(min_length))
@ -477,7 +475,7 @@ class CharField(WritableField):
if isinstance(value, six.string_types): if isinstance(value, six.string_types):
return value return value
if value is None: if value is None and not self.allow_none:
return '' return ''
return smart_text(value) return smart_text(value)
@ -488,7 +486,7 @@ class URLField(CharField):
type_label = 'url' type_label = 'url'
def __init__(self, **kwargs): def __init__(self, **kwargs):
if not 'validators' in kwargs: if 'validators' not in kwargs:
kwargs['validators'] = [validators.URLValidator()] kwargs['validators'] = [validators.URLValidator()]
super(URLField, self).__init__(**kwargs) super(URLField, self).__init__(**kwargs)

View File

@ -5,7 +5,8 @@ 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 rest_framework.compat import django_filters, six, guardian, get_model_name from django.utils import six
from rest_framework.compat import django_filters, guardian, get_model_name
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from functools import reduce from functools import reduce
import operator import operator
@ -44,7 +45,7 @@ class DjangoFilterBackend(BaseFilterBackend):
if filter_class: if filter_class:
filter_model = filter_class.Meta.model filter_model = filter_class.Meta.model
assert issubclass(filter_model, queryset.model), \ assert issubclass(queryset.model, filter_model), \
'FilterSet model %s does not match queryset model %s' % \ 'FilterSet model %s does not match queryset model %s' % \
(filter_model, queryset.model) (filter_model, queryset.model)
@ -116,7 +117,7 @@ class OrderingFilter(BaseFilterBackend):
def get_ordering(self, request): def get_ordering(self, request):
""" """
Ordering is set by a comma delimited ?ordering=... query parameter. Ordering is set by a comma delimited ?ordering=... query parameter.
The `ordering` query parameter can be overridden by setting The `ordering` query parameter can be overridden by setting
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.

View File

@ -25,6 +25,7 @@ def strict_positive_int(integer_string, cutoff=None):
ret = min(ret, cutoff) ret = min(ret, cutoff)
return ret return ret
def get_object_or_404(queryset, *filter_args, **filter_kwargs): def get_object_or_404(queryset, *filter_args, **filter_kwargs):
""" """
Same as Django's standard shortcut, but make sure to raise 404 Same as Django's standard shortcut, but make sure to raise 404
@ -127,11 +128,11 @@ class GenericAPIView(views.APIView):
deprecated_style = False deprecated_style = False
if page_size is not None: if page_size is not None:
warnings.warn('The `page_size` parameter to `paginate_queryset()` ' warnings.warn('The `page_size` parameter to `paginate_queryset()` '
'is due to be deprecated. ' 'is deprecated. '
'Note that the return style of this method is also ' 'Note that the return style of this method is also '
'changed, and will simply return a page object ' 'changed, and will simply return a page object '
'when called without a `page_size` argument.', 'when called without a `page_size` argument.',
PendingDeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
deprecated_style = True deprecated_style = True
else: else:
# Determine the required page size. # Determine the required page size.
@ -142,10 +143,10 @@ class GenericAPIView(views.APIView):
if not self.allow_empty: if not self.allow_empty:
warnings.warn( warnings.warn(
'The `allow_empty` parameter is due to be deprecated. ' 'The `allow_empty` parameter is deprecated. '
'To use `allow_empty=False` style behavior, You should override ' 'To use `allow_empty=False` style behavior, You should override '
'`get_queryset()` and explicitly raise a 404 on empty querysets.', '`get_queryset()` and explicitly raise a 404 on empty querysets.',
PendingDeprecationWarning, stacklevel=2 DeprecationWarning, stacklevel=2
) )
paginator = self.paginator_class(queryset, page_size, paginator = self.paginator_class(queryset, page_size,
@ -162,10 +163,11 @@ class GenericAPIView(views.APIView):
raise Http404(_("Page is not 'last', nor can it be converted to an int.")) raise Http404(_("Page is not 'last', nor can it be converted to an int."))
try: try:
page = paginator.page(page_number) page = paginator.page(page_number)
except InvalidPage as e: except InvalidPage as exc:
raise Http404(_('Invalid page (%(page_number)s): %(message)s') % { error_format = _('Invalid page (%(page_number)s): %(message)s')
'page_number': page_number, raise Http404(error_format % {
'message': str(e) 'page_number': page_number,
'message': str(exc)
}) })
if deprecated_style: if deprecated_style:
@ -199,19 +201,17 @@ class GenericAPIView(views.APIView):
if not filter_backends and self.filter_backend: if not filter_backends and self.filter_backend:
warnings.warn( warnings.warn(
'The `filter_backend` attribute and `FILTER_BACKEND` setting ' 'The `filter_backend` attribute and `FILTER_BACKEND` setting '
'are due to be deprecated in favor of a `filter_backends` ' 'are deprecated in favor of a `filter_backends` '
'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take ' 'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take '
'a *list* of filter backend classes.', 'a *list* of filter backend classes.',
PendingDeprecationWarning, stacklevel=2 DeprecationWarning, stacklevel=2
) )
filter_backends = [self.filter_backend] filter_backends = [self.filter_backend]
return filter_backends return filter_backends
# The following methods provide default implementations
######################## # that you may want to override for more complex cases.
### The following methods provide default implementations
### that you may want to override for more complex cases.
def get_paginate_by(self, queryset=None): def get_paginate_by(self, queryset=None):
""" """
@ -224,8 +224,8 @@ class GenericAPIView(views.APIView):
""" """
if queryset is not None: if queryset is not None:
warnings.warn('The `queryset` parameter to `get_paginate_by()` ' warnings.warn('The `queryset` parameter to `get_paginate_by()` '
'is due to be deprecated.', 'is deprecated.',
PendingDeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
if self.paginate_by_param: if self.paginate_by_param:
try: try:
@ -284,8 +284,8 @@ class GenericAPIView(views.APIView):
if self.model is not None: if self.model is not None:
return self.model._default_manager.all() return self.model._default_manager.all()
raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" error_format = "'%s' must define 'queryset' or 'model'"
% self.__class__.__name__) raise ImproperlyConfigured(error_format % self.__class__.__name__)
def get_object(self, queryset=None): def get_object(self, queryset=None):
""" """
@ -312,16 +312,16 @@ class GenericAPIView(views.APIView):
filter_kwargs = {self.lookup_field: lookup} filter_kwargs = {self.lookup_field: lookup}
elif pk is not None and self.lookup_field == 'pk': elif pk is not None and self.lookup_field == 'pk':
warnings.warn( warnings.warn(
'The `pk_url_kwarg` attribute is due to be deprecated. ' 'The `pk_url_kwarg` attribute is deprecated. '
'Use the `lookup_field` attribute instead', 'Use the `lookup_field` attribute instead',
PendingDeprecationWarning DeprecationWarning
) )
filter_kwargs = {'pk': pk} filter_kwargs = {'pk': pk}
elif slug is not None and self.lookup_field == 'pk': elif slug is not None and self.lookup_field == 'pk':
warnings.warn( warnings.warn(
'The `slug_url_kwarg` attribute is due to be deprecated. ' 'The `slug_url_kwarg` attribute is deprecated. '
'Use the `lookup_field` attribute instead', 'Use the `lookup_field` attribute instead',
PendingDeprecationWarning DeprecationWarning
) )
filter_kwargs = {self.slug_field: slug} filter_kwargs = {self.slug_field: slug}
else: else:
@ -339,12 +339,11 @@ class GenericAPIView(views.APIView):
return obj return obj
######################## # The following are placeholder methods,
### The following are placeholder methods, # and are intended to be overridden.
### and are intended to be overridden. #
### # The are not called by GenericAPIView directly,
### The are not called by GenericAPIView directly, # but are used by the mixin methods.
### but are used by the mixin methods.
def pre_save(self, obj): def pre_save(self, obj):
""" """
@ -416,10 +415,8 @@ class GenericAPIView(views.APIView):
return ret return ret
########################################################## # Concrete view classes that provide method handlers
### Concrete view classes that provide method handlers ### # by composing the mixin classes with the base view.
### by composing the mixin classes with the base view. ###
##########################################################
class CreateAPIView(mixins.CreateModelMixin, class CreateAPIView(mixins.CreateModelMixin,
GenericAPIView): GenericAPIView):
@ -534,16 +531,14 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
return self.destroy(request, *args, **kwargs) return self.destroy(request, *args, **kwargs)
########################## # Deprecated classes
### Deprecated classes ###
##########################
class MultipleObjectAPIView(GenericAPIView): class MultipleObjectAPIView(GenericAPIView):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
warnings.warn( warnings.warn(
'Subclassing `MultipleObjectAPIView` is due to be deprecated. ' 'Subclassing `MultipleObjectAPIView` is deprecated. '
'You should simply subclass `GenericAPIView` instead.', 'You should simply subclass `GenericAPIView` instead.',
PendingDeprecationWarning, stacklevel=2 DeprecationWarning, stacklevel=2
) )
super(MultipleObjectAPIView, self).__init__(*args, **kwargs) super(MultipleObjectAPIView, self).__init__(*args, **kwargs)
@ -551,8 +546,8 @@ class MultipleObjectAPIView(GenericAPIView):
class SingleObjectAPIView(GenericAPIView): class SingleObjectAPIView(GenericAPIView):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
warnings.warn( warnings.warn(
'Subclassing `SingleObjectAPIView` is due to be deprecated. ' 'Subclassing `SingleObjectAPIView` is deprecated. '
'You should simply subclass `GenericAPIView` instead.', 'You should simply subclass `GenericAPIView` instead.',
PendingDeprecationWarning, stacklevel=2 DeprecationWarning, stacklevel=2
) )
super(SingleObjectAPIView, self).__init__(*args, **kwargs) super(SingleObjectAPIView, self).__init__(*args, **kwargs)

View File

@ -26,14 +26,14 @@ def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None)
include = [] include = []
if pk: if pk:
# Pending deprecation # Deprecated
pk_field = obj._meta.pk pk_field = obj._meta.pk
while pk_field.rel: while pk_field.rel:
pk_field = pk_field.rel.to._meta.pk pk_field = pk_field.rel.to._meta.pk
include.append(pk_field.name) include.append(pk_field.name)
if slug_field: if slug_field:
# Pending deprecation # Deprecated
include.append(slug_field) include.append(slug_field)
if lookup_field and lookup_field != 'pk': if lookup_field and lookup_field != 'pk':
@ -79,10 +79,10 @@ class ListModelMixin(object):
# `.allow_empty = False`, to raise 404 errors on empty querysets. # `.allow_empty = False`, to raise 404 errors on empty querysets.
if not self.allow_empty and not self.object_list: if not self.allow_empty and not self.object_list:
warnings.warn( warnings.warn(
'The `allow_empty` parameter is due to be deprecated. ' 'The `allow_empty` parameter is deprecated. '
'To use `allow_empty=False` style behavior, You should override ' 'To use `allow_empty=False` style behavior, You should override '
'`get_queryset()` and explicitly raise a 404 on empty querysets.', '`get_queryset()` and explicitly raise a 404 on empty querysets.',
PendingDeprecationWarning DeprecationWarning
) )
class_name = self.__class__.__name__ class_name = self.__class__.__name__
error_msg = self.empty_error % {'class_name': class_name} error_msg = self.empty_error % {'class_name': class_name}

View File

@ -54,8 +54,10 @@ class DefaultContentNegotiation(BaseContentNegotiation):
for media_type in media_type_set: for media_type in media_type_set:
if media_type_matches(renderer.media_type, media_type): if media_type_matches(renderer.media_type, media_type):
# Return the most specific media type as accepted. # Return the most specific media type as accepted.
if (_MediaType(renderer.media_type).precedence > if (
_MediaType(media_type).precedence): _MediaType(renderer.media_type).precedence >
_MediaType(media_type).precedence
):
# Eg client requests '*/*' # Eg client requests '*/*'
# Accepted media type is 'application/json' # Accepted media type is 'application/json'
return renderer, renderer.media_type return renderer, renderer.media_type

View File

@ -10,7 +10,8 @@ from django.core.files.uploadhandler import StopFutureHandlers
from django.http import QueryDict from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
from rest_framework.compat import etree, six, yaml, force_text from django.utils import six
from rest_framework.compat import etree, yaml, force_text
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from rest_framework import renderers from rest_framework import renderers
import json import json

View File

@ -2,15 +2,12 @@
Provides a set of pluggable permission policies. Provides a set of pluggable permission policies.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import inspect
import warnings
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
from django.http import Http404 from django.http import Http404
from rest_framework.compat import (get_model_name, oauth2_provider_scope, from rest_framework.compat import (get_model_name, oauth2_provider_scope,
oauth2_constants) oauth2_constants)
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
class BasePermission(object): class BasePermission(object):
""" """
@ -27,13 +24,6 @@ class BasePermission(object):
""" """
Return `True` if permission is granted, `False` otherwise. Return `True` if permission is granted, `False` otherwise.
""" """
if len(inspect.getargspec(self.has_permission).args) == 4:
warnings.warn(
'The `obj` argument in `has_permission` is deprecated. '
'Use `has_object_permission()` instead for object permissions.',
DeprecationWarning, stacklevel=2
)
return self.has_permission(request, view, obj)
return True return True
@ -72,9 +62,11 @@ class IsAuthenticatedOrReadOnly(BasePermission):
""" """
def has_permission(self, request, view): def has_permission(self, request, view):
return (request.method in SAFE_METHODS or return (
request.user and request.method in SAFE_METHODS or
request.user.is_authenticated()) request.user and
request.user.is_authenticated()
)
class DjangoModelPermissions(BasePermission): class DjangoModelPermissions(BasePermission):
@ -132,9 +124,11 @@ class DjangoModelPermissions(BasePermission):
perms = self.get_required_permissions(request.method, model_cls) perms = self.get_required_permissions(request.method, model_cls)
return (request.user and return (
request.user and
(request.user.is_authenticated() or not self.authenticated_users_only) and (request.user.is_authenticated() or not self.authenticated_users_only) and
request.user.has_perms(perms)) request.user.has_perms(perms)
)
class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
@ -222,6 +216,8 @@ class TokenHasReadWriteScope(BasePermission):
required = oauth2_constants.READ if read_only else oauth2_constants.WRITE required = oauth2_constants.READ if read_only else oauth2_constants.WRITE
return oauth2_provider_scope.check(required, request.auth.scope) return oauth2_provider_scope.check(required, request.auth.scope)
assert False, ('TokenHasReadWriteScope requires either the' assert False, (
'`OAuthAuthentication` or `OAuth2Authentication` authentication ' 'TokenHasReadWriteScope requires either the'
'class to be used.') '`OAuthAuthentication` or `OAuth2Authentication` authentication '
'class to be used.'
)

View File

@ -19,8 +19,7 @@ from rest_framework.compat import smart_text
import warnings import warnings
##### Relational fields ##### # Relational fields
# Not actually Writable, but subclasses may need to be. # Not actually Writable, but subclasses may need to be.
class RelatedField(WritableField): class RelatedField(WritableField):
@ -41,14 +40,6 @@ class RelatedField(WritableField):
many = False many = False
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# 'null' is to be deprecated in favor of 'required'
if 'null' in kwargs:
warnings.warn('The `null` keyword argument is deprecated. '
'Use the `required` keyword argument instead.',
DeprecationWarning, stacklevel=2)
kwargs['required'] = not kwargs.pop('null')
queryset = kwargs.pop('queryset', None) queryset = kwargs.pop('queryset', None)
self.many = kwargs.pop('many', self.many) self.many = kwargs.pop('many', self.many)
if self.many: if self.many:
@ -74,7 +65,7 @@ class RelatedField(WritableField):
else: # Reverse else: # Reverse
self.queryset = manager.field.rel.to._default_manager.all() self.queryset = manager.field.rel.to._default_manager.all()
### We need this stuff to make form choices work... # We need this stuff to make form choices work...
def prepare_value(self, obj): def prepare_value(self, obj):
return self.to_native(obj) return self.to_native(obj)
@ -121,7 +112,7 @@ class RelatedField(WritableField):
choices = property(_get_choices, _set_choices) choices = property(_get_choices, _set_choices)
### Default value handling # Default value handling
def get_default_value(self): def get_default_value(self):
default = super(RelatedField, self).get_default_value() default = super(RelatedField, self).get_default_value()
@ -129,7 +120,7 @@ class RelatedField(WritableField):
return [] return []
return default return default
### Regular serializer stuff... # Regular serializer stuff...
def field_to_native(self, obj, field_name): def field_to_native(self, obj, field_name):
try: try:
@ -189,7 +180,7 @@ class RelatedField(WritableField):
into[(self.source or field_name)] = self.from_native(value) into[(self.source or field_name)] = self.from_native(value)
### PrimaryKey relationships # PrimaryKey relationships
class PrimaryKeyRelatedField(RelatedField): class PrimaryKeyRelatedField(RelatedField):
""" """
@ -277,8 +268,7 @@ class PrimaryKeyRelatedField(RelatedField):
return self.to_native(pk) return self.to_native(pk)
### Slug relationships # Slug relationships
class SlugRelatedField(RelatedField): class SlugRelatedField(RelatedField):
""" """
@ -313,7 +303,7 @@ class SlugRelatedField(RelatedField):
raise ValidationError(msg) raise ValidationError(msg)
### Hyperlinked relationships # Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField): class HyperlinkedRelatedField(RelatedField):
""" """
@ -330,7 +320,7 @@ class HyperlinkedRelatedField(RelatedField):
'incorrect_type': _('Incorrect type. Expected url string, received %s.'), 'incorrect_type': _('Incorrect type. Expected url string, received %s.'),
} }
# These are all pending deprecation # These are all deprecated
pk_url_kwarg = 'pk' pk_url_kwarg = 'pk'
slug_field = 'slug' slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
@ -344,16 +334,16 @@ class HyperlinkedRelatedField(RelatedField):
self.lookup_field = kwargs.pop('lookup_field', self.lookup_field) self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
self.format = kwargs.pop('format', None) self.format = kwargs.pop('format', None)
# These are pending deprecation # These are deprecated
if 'pk_url_kwarg' in kwargs: if 'pk_url_kwarg' in kwargs:
msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.' msg = 'pk_url_kwarg is deprecated. Use lookup_field instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) warnings.warn(msg, DeprecationWarning, stacklevel=2)
if 'slug_url_kwarg' in kwargs: if 'slug_url_kwarg' in kwargs:
msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.' msg = 'slug_url_kwarg is deprecated. Use lookup_field instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) warnings.warn(msg, DeprecationWarning, stacklevel=2)
if 'slug_field' in kwargs: if 'slug_field' in kwargs:
msg = 'slug_field is pending deprecation. Use lookup_field instead.' msg = 'slug_field is deprecated. Use lookup_field instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) warnings.warn(msg, DeprecationWarning, stacklevel=2)
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg) self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
self.slug_field = kwargs.pop('slug_field', self.slug_field) self.slug_field = kwargs.pop('slug_field', self.slug_field)
@ -396,9 +386,9 @@ class HyperlinkedRelatedField(RelatedField):
# If the lookup succeeds using the default slug params, # If the lookup succeeds using the default slug params,
# then `slug_field` is being used implicitly, and we # then `slug_field` is being used implicitly, and we
# we need to warn about the pending deprecation. # we need to warn about the pending deprecation.
msg = 'Implicit slug field hyperlinked fields are pending deprecation.' \ msg = 'Implicit slug field hyperlinked fields are deprecated.' \
'You should set `lookup_field=slug` on the HyperlinkedRelatedField.' 'You should set `lookup_field=slug` on the HyperlinkedRelatedField.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) warnings.warn(msg, DeprecationWarning, stacklevel=2)
return ret return ret
except NoReverseMatch: except NoReverseMatch:
pass pass
@ -432,14 +422,11 @@ class HyperlinkedRelatedField(RelatedField):
request = self.context.get('request', None) request = self.context.get('request', None)
format = self.format or self.context.get('format', None) format = self.format or self.context.get('format', None)
if request is None: assert request is not None, (
msg = ( "`HyperlinkedRelatedField` requires the request in the serializer "
"Using `HyperlinkedRelatedField` without including the request " "context. Add `context={'request': request}` when instantiating "
"in the serializer context is deprecated. " "the serializer."
"Add `context={'request': request}` when instantiating " )
"the serializer."
)
warnings.warn(msg, DeprecationWarning, stacklevel=4)
# If the object has not yet been saved then we cannot hyperlink to it. # If the object has not yet been saved then we cannot hyperlink to it.
if getattr(obj, 'pk', None) is None: if getattr(obj, 'pk', None) is None:
@ -499,7 +486,7 @@ class HyperlinkedIdentityField(Field):
lookup_field = 'pk' lookup_field = 'pk'
read_only = True read_only = True
# These are all pending deprecation # These are all deprecated
pk_url_kwarg = 'pk' pk_url_kwarg = 'pk'
slug_field = 'slug' slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
@ -515,16 +502,16 @@ class HyperlinkedIdentityField(Field):
lookup_field = kwargs.pop('lookup_field', None) lookup_field = kwargs.pop('lookup_field', None)
self.lookup_field = lookup_field or self.lookup_field self.lookup_field = lookup_field or self.lookup_field
# These are pending deprecation # These are deprecated
if 'pk_url_kwarg' in kwargs: if 'pk_url_kwarg' in kwargs:
msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.' msg = 'pk_url_kwarg is deprecated. Use lookup_field instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) warnings.warn(msg, DeprecationWarning, stacklevel=2)
if 'slug_url_kwarg' in kwargs: if 'slug_url_kwarg' in kwargs:
msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.' msg = 'slug_url_kwarg is deprecated. Use lookup_field instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) warnings.warn(msg, DeprecationWarning, stacklevel=2)
if 'slug_field' in kwargs: if 'slug_field' in kwargs:
msg = 'slug_field is pending deprecation. Use lookup_field instead.' msg = 'slug_field is deprecated. Use lookup_field instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) warnings.warn(msg, DeprecationWarning, stacklevel=2)
self.slug_field = kwargs.pop('slug_field', self.slug_field) self.slug_field = kwargs.pop('slug_field', self.slug_field)
default_slug_kwarg = self.slug_url_kwarg or self.slug_field default_slug_kwarg = self.slug_url_kwarg or self.slug_field
@ -538,11 +525,11 @@ class HyperlinkedIdentityField(Field):
format = self.context.get('format', None) format = self.context.get('format', None)
view_name = self.view_name view_name = self.view_name
if request is None: assert request is not None, (
warnings.warn("Using `HyperlinkedIdentityField` without including the " "`HyperlinkedIdentityField` requires the request in the serializer"
"request in the serializer context is deprecated. " " context. Add `context={'request': request}` when instantiating "
"Add `context={'request': request}` when instantiating the serializer.", "the serializer."
DeprecationWarning, stacklevel=4) )
# By default use whatever format is given for the current context # By default use whatever format is given for the current context
# unless the target is a different type to the source. # unless the target is a different type to the source.
@ -606,41 +593,3 @@ class HyperlinkedIdentityField(Field):
pass pass
raise NoReverseMatch() raise NoReverseMatch()
### Old-style many classes for backwards compat
class ManyRelatedField(RelatedField):
def __init__(self, *args, **kwargs):
warnings.warn('`ManyRelatedField()` is deprecated. '
'Use `RelatedField(many=True)` instead.',
DeprecationWarning, stacklevel=2)
kwargs['many'] = True
super(ManyRelatedField, self).__init__(*args, **kwargs)
class ManyPrimaryKeyRelatedField(PrimaryKeyRelatedField):
def __init__(self, *args, **kwargs):
warnings.warn('`ManyPrimaryKeyRelatedField()` is deprecated. '
'Use `PrimaryKeyRelatedField(many=True)` instead.',
DeprecationWarning, stacklevel=2)
kwargs['many'] = True
super(ManyPrimaryKeyRelatedField, self).__init__(*args, **kwargs)
class ManySlugRelatedField(SlugRelatedField):
def __init__(self, *args, **kwargs):
warnings.warn('`ManySlugRelatedField()` is deprecated. '
'Use `SlugRelatedField(many=True)` instead.',
DeprecationWarning, stacklevel=2)
kwargs['many'] = True
super(ManySlugRelatedField, self).__init__(*args, **kwargs)
class ManyHyperlinkedRelatedField(HyperlinkedRelatedField):
def __init__(self, *args, **kwargs):
warnings.warn('`ManyHyperlinkedRelatedField()` is deprecated. '
'Use `HyperlinkedRelatedField(many=True)` instead.',
DeprecationWarning, stacklevel=2)
kwargs['many'] = True
super(ManyHyperlinkedRelatedField, self).__init__(*args, **kwargs)

View File

@ -8,7 +8,6 @@ REST framework also provides an HTML renderer the renders the browsable API.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import copy
import json import json
import django import django
from django import forms from django import forms
@ -16,11 +15,9 @@ from django.core.exceptions import ImproperlyConfigured
from django.http.multipartparser import parse_header from django.http.multipartparser import parse_header
from django.template import RequestContext, loader, Template from django.template import RequestContext, loader, Template
from django.test.client import encode_multipart from django.test.client import encode_multipart
from django.utils import six
from django.utils.xmlutils import SimplerXMLGenerator from django.utils.xmlutils import SimplerXMLGenerator
from rest_framework.compat import StringIO from rest_framework.compat import StringIO, smart_text, yaml
from rest_framework.compat import six
from rest_framework.compat import smart_text
from rest_framework.compat import yaml
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.request import is_form_media_type, override_method from rest_framework.request import is_form_media_type, override_method
@ -75,7 +72,6 @@ class JSONRenderer(BaseRenderer):
# E.g. If we're being called by the BrowsableAPIRenderer. # E.g. If we're being called by the BrowsableAPIRenderer.
return renderer_context.get('indent', None) return renderer_context.get('indent', None)
def render(self, data, accepted_media_type=None, renderer_context=None): def render(self, data, accepted_media_type=None, renderer_context=None):
""" """
Render `data` into JSON, returning a bytestring. Render `data` into JSON, returning a bytestring.
@ -86,8 +82,10 @@ class JSONRenderer(BaseRenderer):
renderer_context = renderer_context or {} renderer_context = renderer_context or {}
indent = self.get_indent(accepted_media_type, renderer_context) indent = self.get_indent(accepted_media_type, renderer_context)
ret = json.dumps(data, cls=self.encoder_class, ret = json.dumps(
indent=indent, ensure_ascii=self.ensure_ascii) data, cls=self.encoder_class,
indent=indent, ensure_ascii=self.ensure_ascii
)
# On python 2.x json.dumps() returns bytestrings if ensure_ascii=True, # On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,
# but if ensure_ascii=False, the return type is underspecified, # but if ensure_ascii=False, the return type is underspecified,
@ -414,7 +412,7 @@ class BrowsableAPIRenderer(BaseRenderer):
""" """
Returns True if a form should be shown for this method. Returns True if a form should be shown for this method.
""" """
if not method in view.allowed_methods: if method not in view.allowed_methods:
return # Not a valid method return # Not a valid method
if not api_settings.FORM_METHOD_OVERRIDE: if not api_settings.FORM_METHOD_OVERRIDE:
@ -454,8 +452,10 @@ class BrowsableAPIRenderer(BaseRenderer):
if method in ('DELETE', 'OPTIONS'): if method in ('DELETE', 'OPTIONS'):
return True # Don't actually need to return a form return True # Don't actually need to return a form
if (not getattr(view, 'get_serializer', None) if (
or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)): not getattr(view, 'get_serializer', None)
or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)
):
return return
serializer = view.get_serializer(instance=obj, data=data, files=files) serializer = view.get_serializer(instance=obj, data=data, files=files)
@ -576,7 +576,7 @@ class BrowsableAPIRenderer(BaseRenderer):
'version': VERSION, 'version': VERSION,
'breadcrumblist': self.get_breadcrumbs(request), 'breadcrumblist': self.get_breadcrumbs(request),
'allowed_methods': view.allowed_methods, 'allowed_methods': view.allowed_methods,
'available_formats': [renderer.format for renderer in view.renderer_classes], 'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
'response_headers': response_headers, 'response_headers': response_headers,
'put_form': self.get_rendered_html_form(view, 'PUT', request), 'put_form': self.get_rendered_html_form(view, 'PUT', request),
@ -625,4 +625,3 @@ class MultiPartRenderer(BaseRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None): def render(self, data, accepted_media_type=None, renderer_context=None):
return encode_multipart(self.BOUNDARY, data) return encode_multipart(self.BOUNDARY, data)

View File

@ -295,8 +295,11 @@ class Request(object):
Return the content body of the request, as a stream. Return the content body of the request, as a stream.
""" """
try: try:
content_length = int(self.META.get('CONTENT_LENGTH', content_length = int(
self.META.get('HTTP_CONTENT_LENGTH'))) self.META.get(
'CONTENT_LENGTH', self.META.get('HTTP_CONTENT_LENGTH')
)
)
except (ValueError, TypeError): except (ValueError, TypeError):
content_length = 0 content_length = 0
@ -320,9 +323,11 @@ class Request(object):
) )
# We only need to use form overloading on form POST requests. # We only need to use form overloading on form POST requests.
if (not USE_FORM_OVERLOADING if (
not USE_FORM_OVERLOADING
or self._request.method != 'POST' or self._request.method != 'POST'
or not is_form_media_type(self._content_type)): or not is_form_media_type(self._content_type)
):
return return
# 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.
@ -330,15 +335,19 @@ class Request(object):
self._files = self._request.FILES self._files = self._request.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 (self._METHOD_PARAM and if (
self._METHOD_PARAM in self._data): self._METHOD_PARAM and
self._METHOD_PARAM in self._data
):
self._method = self._data[self._METHOD_PARAM].upper() self._method = self._data[self._METHOD_PARAM].upper()
# Content overloading - modify the content type, and force re-parse. # Content overloading - modify the content type, and force re-parse.
if (self._CONTENT_PARAM and if (
self._CONTENT_PARAM and
self._CONTENTTYPE_PARAM and self._CONTENTTYPE_PARAM and
self._CONTENT_PARAM in self._data and self._CONTENT_PARAM in self._data and
self._CONTENTTYPE_PARAM in self._data): self._CONTENTTYPE_PARAM in self._data
):
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 = (Empty, Empty)
@ -394,7 +403,7 @@ class Request(object):
self._not_authenticated() self._not_authenticated()
raise raise
if not user_auth_tuple is None: if user_auth_tuple is not None:
self._authenticator = authenticator self._authenticator = authenticator
self._user, self._auth = user_auth_tuple self._user, self._auth = user_auth_tuple
return return

View File

@ -8,7 +8,7 @@ from __future__ import unicode_literals
import django import django
from django.core.handlers.wsgi import STATUS_CODE_TEXT from django.core.handlers.wsgi import STATUS_CODE_TEXT
from django.template.response import SimpleTemplateResponse from django.template.response import SimpleTemplateResponse
from rest_framework.compat import six from django.utils import six
class Response(SimpleTemplateResponse): class Response(SimpleTemplateResponse):
@ -62,8 +62,10 @@ class Response(SimpleTemplateResponse):
ret = renderer.render(self.data, media_type, context) ret = renderer.render(self.data, media_type, context)
if isinstance(ret, six.text_type): if isinstance(ret, six.text_type):
assert charset, 'renderer returned unicode, and did not specify ' \ assert charset, (
'a charset value.' 'renderer returned unicode, and did not specify '
'a charset value.'
)
return bytes(ret.encode(charset)) return bytes(ret.encode(charset))
if not ret: if not ret:

View File

@ -17,15 +17,17 @@ from __future__ import unicode_literals
import itertools import itertools
from collections import namedtuple from collections import namedtuple
from django.conf.urls import patterns, url
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from rest_framework import views from rest_framework import views
from rest_framework.compat import patterns, url
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
Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs']) Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs'])
DynamicDetailRoute = namedtuple('DynamicDetailRoute', ['url', 'name', 'initkwargs'])
DynamicListRoute = namedtuple('DynamicListRoute', ['url', 'name', 'initkwargs'])
def replace_methodname(format_string, methodname): def replace_methodname(format_string, methodname):
@ -88,6 +90,14 @@ class SimpleRouter(BaseRouter):
name='{basename}-list', name='{basename}-list',
initkwargs={'suffix': 'List'} initkwargs={'suffix': 'List'}
), ),
# Dynamically generated list routes.
# Generated using @list_route decorator
# on methods of the viewset.
DynamicListRoute(
url=r'^{prefix}/{methodname}{trailing_slash}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
),
# Detail route. # Detail route.
Route( Route(
url=r'^{prefix}/{lookup}{trailing_slash}$', url=r'^{prefix}/{lookup}{trailing_slash}$',
@ -100,13 +110,10 @@ class SimpleRouter(BaseRouter):
name='{basename}-detail', name='{basename}-detail',
initkwargs={'suffix': 'Instance'} initkwargs={'suffix': 'Instance'}
), ),
# Dynamically generated routes. # Dynamically generated detail routes.
# Generated using @action or @link decorators on methods of the viewset. # Generated using @detail_route decorator on methods of the viewset.
Route( DynamicDetailRoute(
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$', url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
mapping={
'{httpmethod}': '{methodname}',
},
name='{basename}-{methodnamehyphen}', name='{basename}-{methodnamehyphen}',
initkwargs={} initkwargs={}
), ),
@ -139,25 +146,42 @@ class SimpleRouter(BaseRouter):
Returns a list of the Route namedtuple. Returns a list of the Route namedtuple.
""" """
known_actions = flatten([route.mapping.values() for route in self.routes]) known_actions = flatten([route.mapping.values() for route in self.routes if isinstance(route, Route)])
# Determine any `@action` or `@link` decorated methods on the viewset # Determine any `@detail_route` or `@list_route` decorated methods on the viewset
dynamic_routes = [] detail_routes = []
list_routes = []
for methodname in dir(viewset): for methodname in dir(viewset):
attr = getattr(viewset, methodname) attr = getattr(viewset, methodname)
httpmethods = getattr(attr, 'bind_to_methods', None) httpmethods = getattr(attr, 'bind_to_methods', None)
detail = getattr(attr, 'detail', True)
if httpmethods: if httpmethods:
if methodname in known_actions: if methodname in known_actions:
raise ImproperlyConfigured('Cannot use @action or @link decorator on ' raise ImproperlyConfigured('Cannot use @detail_route or @list_route '
'method "%s" as it is an existing route' % methodname) 'decorators on method "%s" '
'as it is an existing route' % methodname)
httpmethods = [method.lower() for method in httpmethods] httpmethods = [method.lower() for method in httpmethods]
dynamic_routes.append((httpmethods, methodname)) if detail:
detail_routes.append((httpmethods, methodname))
else:
list_routes.append((httpmethods, methodname))
ret = [] ret = []
for route in self.routes: for route in self.routes:
if route.mapping == {'{httpmethod}': '{methodname}'}: if isinstance(route, DynamicDetailRoute):
# Dynamic routes (@link or @action decorator) # Dynamic detail routes (@detail_route decorator)
for httpmethods, methodname in dynamic_routes: for httpmethods, methodname in detail_routes:
initkwargs = route.initkwargs.copy()
initkwargs.update(getattr(viewset, methodname).kwargs)
ret.append(Route(
url=replace_methodname(route.url, methodname),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
name=replace_methodname(route.name, methodname),
initkwargs=initkwargs,
))
elif isinstance(route, DynamicListRoute):
# Dynamic list routes (@list_route decorator)
for httpmethods, methodname in list_routes:
initkwargs = route.initkwargs.copy() initkwargs = route.initkwargs.copy()
initkwargs.update(getattr(viewset, methodname).kwargs) initkwargs.update(getattr(viewset, methodname).kwargs)
ret.append(Route( ret.append(Route(
@ -195,13 +219,16 @@ class SimpleRouter(BaseRouter):
https://github.com/alanjds/drf-nested-routers https://github.com/alanjds/drf-nested-routers
""" """
if self.trailing_slash: base_regex = '(?P<{lookup_prefix}{lookup_field}>{lookup_value})'
base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/]+)' # Use `pk` as default field, unset set. Default regex should not
else: # consume `.json` style suffixes and should break at '/' boundaries.
# Don't consume `.json` style suffixes
base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/.]+)'
lookup_field = getattr(viewset, 'lookup_field', 'pk') lookup_field = getattr(viewset, 'lookup_field', 'pk')
return base_regex.format(lookup_field=lookup_field, lookup_prefix=lookup_prefix) lookup_value = getattr(viewset, 'lookup_value_regex', '[^/.]+')
return base_regex.format(
lookup_prefix=lookup_prefix,
lookup_field=lookup_field,
lookup_value=lookup_value
)
def get_urls(self): def get_urls(self):
""" """

View File

@ -1,78 +0,0 @@
#!/usr/bin/env python
"""
Useful tool to run the test suite for rest_framework and generate a coverage report.
"""
# http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/
# http://www.travisswicegood.com/2010/01/17/django-virtualenv-pip-and-fabric/
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
import os
import sys
# fix sys path so we don't need to setup PYTHONPATH
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
from coverage import coverage
def main():
"""Run the tests for rest_framework and generate a coverage report."""
cov = coverage()
cov.erase()
cov.start()
from django.conf import settings
from django.test.utils import get_runner
TestRunner = get_runner(settings)
if hasattr(TestRunner, 'func_name'):
# Pre 1.2 test runners were just functions,
# and did not support the 'failfast' option.
import warnings
warnings.warn(
'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.',
DeprecationWarning
)
failures = TestRunner(['tests'])
else:
test_runner = TestRunner()
failures = test_runner.run_tests(['tests'])
cov.stop()
# Discover the list of all modules that we should test coverage for
import rest_framework
project_dir = os.path.dirname(rest_framework.__file__)
cov_files = []
for (path, dirs, files) in os.walk(project_dir):
# Drop tests and runtests directories from the test coverage report
if os.path.basename(path) in ['tests', 'runtests', 'migrations']:
continue
# Drop the compat and six modules from coverage, since we're not interested in the coverage
# of modules which are specifically for resolving environment dependant imports.
# (Because we'll end up getting different coverage reports for it for each environment)
if 'compat.py' in files:
files.remove('compat.py')
if 'six.py' in files:
files.remove('six.py')
# Same applies to template tags module.
# This module has to include branching on Django versions,
# so it's never possible for it to have full coverage.
if 'rest_framework.py' in files:
files.remove('rest_framework.py')
cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
cov.report(cov_files)
if '--html' in sys.argv:
cov.html_report(cov_files, directory='coverage')
sys.exit(failures)
if __name__ == '__main__':
main()

View File

@ -1,52 +0,0 @@
#!/usr/bin/env python
# http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/
# http://www.travisswicegood.com/2010/01/17/django-virtualenv-pip-and-fabric/
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
import os
import sys
# fix sys path so we don't need to setup PYTHONPATH
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
import django
from django.conf import settings
from django.test.utils import get_runner
def usage():
return """
Usage: python runtests.py [UnitTestClass].[method]
You can pass the Class name of the `UnitTestClass` you want to test.
Append a method name if you only want to test a specific method of that class.
"""
def main():
try:
django.setup()
except AttributeError:
pass
TestRunner = get_runner(settings)
test_runner = TestRunner()
if len(sys.argv) == 2:
test_case = '.' + sys.argv[1]
elif len(sys.argv) == 1:
test_case = ''
else:
print(usage())
sys.exit(1)
test_module_name = 'rest_framework.tests'
if django.VERSION[0] == 1 and django.VERSION[1] < 6:
test_module_name = 'tests'
failures = test_runner.run_tests([test_module_name + test_case])
sys.exit(failures)
if __name__ == '__main__':
main()

View File

@ -1,7 +0,0 @@
"""
Blank URLConf just to keep runtests.py happy.
"""
from rest_framework.compat import patterns
urlpatterns = patterns('',
)

View File

@ -20,9 +20,9 @@ from django.contrib.contenttypes.generic import GenericForeignKey
from django.core.paginator import Page from django.core.paginator import Page
from django.db import models from django.db import models
from django.forms import widgets from django.forms import widgets
from django.utils import six
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from rest_framework.compat import get_concrete_model, six
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -182,7 +182,7 @@ class BaseSerializer(WritableField):
_dict_class = SortedDictWithMetadata _dict_class = SortedDictWithMetadata
def __init__(self, instance=None, data=None, files=None, def __init__(self, instance=None, data=None, files=None,
context=None, partial=False, many=None, context=None, partial=False, many=False,
allow_add_remove=False, **kwargs): allow_add_remove=False, **kwargs):
super(BaseSerializer, self).__init__(**kwargs) super(BaseSerializer, self).__init__(**kwargs)
self.opts = self._options_class(self.Meta) self.opts = self._options_class(self.Meta)
@ -412,12 +412,7 @@ class BaseSerializer(WritableField):
if value is None: if value is None:
return None return None
if self.many is not None: if self.many:
many = self.many
else:
many = hasattr(value, '__iter__') and not isinstance(value, (Page, dict, six.text_type))
if many:
return [self.to_native(item) for item in value] return [self.to_native(item) for item in value]
return self.to_native(value) return self.to_native(value)
@ -454,9 +449,11 @@ class BaseSerializer(WritableField):
# If we have a model manager or similar object then we need # If we have a model manager or similar object then we need
# to iterate through each instance. # to iterate through each instance.
if (self.many and if (
self.many and
not hasattr(obj, '__iter__') and not hasattr(obj, '__iter__') and
is_simple_callable(getattr(obj, 'all', None))): is_simple_callable(getattr(obj, 'all', None))
):
obj = obj.all() obj = obj.all()
kwargs = { kwargs = {
@ -606,8 +603,10 @@ class BaseSerializer(WritableField):
API schemas for auto-documentation. API schemas for auto-documentation.
""" """
return SortedDict( return SortedDict(
[(field_name, field.metadata()) [
for field_name, field in six.iteritems(self.fields)] (field_name, field.metadata())
for field_name, field in six.iteritems(self.fields)
]
) )
@ -661,9 +660,11 @@ class ModelSerializer(Serializer):
""" """
cls = self.opts.model cls = self.opts.model
assert cls is not None, \ assert cls is not None, (
"Serializer class '%s' is missing 'model' Meta option" % self.__class__.__name__ "Serializer class '%s' is missing 'model' Meta option" %
opts = get_concrete_model(cls)._meta self.__class__.__name__
)
opts = cls._meta.concrete_model._meta
ret = SortedDict() ret = SortedDict()
nested = bool(self.opts.depth) nested = bool(self.opts.depth)
@ -673,9 +674,9 @@ class ModelSerializer(Serializer):
# If model is a child via multitable inheritance, use parent's pk # If model is a child via multitable inheritance, use parent's pk
pk_field = pk_field.rel.to._meta.pk pk_field = pk_field.rel.to._meta.pk
field = self.get_pk_field(pk_field) serializer_pk_field = self.get_pk_field(pk_field)
if field: if serializer_pk_field:
ret[pk_field.name] = field ret[pk_field.name] = serializer_pk_field
# Deal with forward relationships # Deal with forward relationships
forward_rels = [field for field in opts.fields if field.serialize] forward_rels = [field for field in opts.fields if field.serialize]
@ -696,10 +697,10 @@ class ModelSerializer(Serializer):
if len(inspect.getargspec(self.get_nested_field).args) == 2: if len(inspect.getargspec(self.get_nested_field).args) == 2:
warnings.warn( warnings.warn(
'The `get_nested_field(model_field)` call signature ' 'The `get_nested_field(model_field)` call signature '
'is due to be deprecated. ' 'is deprecated. '
'Use `get_nested_field(model_field, related_model, ' 'Use `get_nested_field(model_field, related_model, '
'to_many) instead', 'to_many) instead',
PendingDeprecationWarning DeprecationWarning
) )
field = self.get_nested_field(model_field) field = self.get_nested_field(model_field)
else: else:
@ -708,10 +709,10 @@ class ModelSerializer(Serializer):
if len(inspect.getargspec(self.get_nested_field).args) == 3: if len(inspect.getargspec(self.get_nested_field).args) == 3:
warnings.warn( warnings.warn(
'The `get_related_field(model_field, to_many)` call ' 'The `get_related_field(model_field, to_many)` call '
'signature is due to be deprecated. ' 'signature is deprecated. '
'Use `get_related_field(model_field, related_model, ' 'Use `get_related_field(model_field, related_model, '
'to_many) instead', 'to_many) instead',
PendingDeprecationWarning DeprecationWarning
) )
field = self.get_related_field(model_field, to_many=to_many) field = self.get_related_field(model_field, to_many=to_many)
else: else:
@ -744,9 +745,11 @@ class ModelSerializer(Serializer):
is_m2m = isinstance(relation.field, is_m2m = isinstance(relation.field,
models.fields.related.ManyToManyField) models.fields.related.ManyToManyField)
if (is_m2m and if (
is_m2m and
hasattr(relation.field.rel, 'through') and hasattr(relation.field.rel, 'through') and
not relation.field.rel.through._meta.auto_created): not relation.field.rel.through._meta.auto_created
):
has_through_model = True has_through_model = True
if nested: if nested:
@ -881,6 +884,10 @@ class ModelSerializer(Serializer):
issubclass(model_field.__class__, models.PositiveSmallIntegerField): issubclass(model_field.__class__, models.PositiveSmallIntegerField):
kwargs['min_value'] = 0 kwargs['min_value'] = 0
if model_field.null and \
issubclass(model_field.__class__, (models.CharField, models.TextField)):
kwargs['allow_none'] = True
attribute_dict = { attribute_dict = {
models.CharField: ['max_length'], models.CharField: ['max_length'],
models.CommaSeparatedIntegerField: ['max_length'], models.CommaSeparatedIntegerField: ['max_length'],
@ -907,15 +914,17 @@ class ModelSerializer(Serializer):
Return a list of field names to exclude from model validation. Return a list of field names to exclude from model validation.
""" """
cls = self.opts.model cls = self.opts.model
opts = get_concrete_model(cls)._meta opts = cls._meta.concrete_model._meta
exclusions = [field.name for field in opts.fields + opts.many_to_many] exclusions = [field.name for field in opts.fields + opts.many_to_many]
for field_name, field in self.fields.items(): for field_name, field in self.fields.items():
field_name = field.source or field_name field_name = field.source or field_name
if field_name in exclusions \ if (
and not field.read_only \ field_name in exclusions
and (field.required or hasattr(instance, field_name)) \ and not field.read_only
and not isinstance(field, Serializer): and (field.required or hasattr(instance, field_name))
and not isinstance(field, Serializer)
):
exclusions.remove(field_name) exclusions.remove(field_name)
return exclusions return exclusions

View File

@ -18,12 +18,9 @@ REST framework settings, checking for user settings first, then falling
back to the defaults. back to the defaults.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.utils import importlib from django.utils import importlib, six
from rest_framework import ISO_8601 from rest_framework import ISO_8601
from rest_framework.compat import six
USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None) USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None)
@ -46,16 +43,12 @@ DEFAULTS = {
'DEFAULT_PERMISSION_CLASSES': ( 'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny', 'rest_framework.permissions.AllowAny',
), ),
'DEFAULT_THROTTLE_CLASSES': ( 'DEFAULT_THROTTLE_CLASSES': (),
), 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
'DEFAULT_CONTENT_NEGOTIATION_CLASS':
'rest_framework.negotiation.DefaultContentNegotiation',
# Genric view behavior # Genric view behavior
'DEFAULT_MODEL_SERIALIZER_CLASS': 'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer',
'rest_framework.serializers.ModelSerializer', 'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'rest_framework.pagination.PaginationSerializer',
'DEFAULT_PAGINATION_SERIALIZER_CLASS':
'rest_framework.pagination.PaginationSerializer',
'DEFAULT_FILTER_BACKENDS': (), 'DEFAULT_FILTER_BACKENDS': (),
# Throttling # Throttling
@ -63,6 +56,7 @@ DEFAULTS = {
'user': None, 'user': None,
'anon': None, 'anon': None,
}, },
'NUM_PROXIES': None,
# Pagination # Pagination
'PAGINATE_BY': None, 'PAGINATE_BY': None,
@ -119,6 +113,7 @@ DEFAULTS = {
# Pending deprecation # Pending deprecation
'FILTER_BACKEND': None, 'FILTER_BACKEND': None,
} }

View File

@ -1,389 +0,0 @@
"""Utilities for writing code that runs on Python 2 and 3"""
import operator
import sys
import types
__author__ = "Benjamin Peterson <benjamin@python.org>"
__version__ = "1.2.0"
# True if we are running on Python 3.
PY3 = sys.version_info[0] == 3
if PY3:
string_types = str,
integer_types = int,
class_types = type,
text_type = str
binary_type = bytes
MAXSIZE = sys.maxsize
else:
string_types = basestring,
integer_types = (int, long)
class_types = (type, types.ClassType)
text_type = unicode
binary_type = str
if sys.platform == "java":
# Jython always uses 32 bits.
MAXSIZE = int((1 << 31) - 1)
else:
# It's possible to have sizeof(long) != sizeof(Py_ssize_t).
class X(object):
def __len__(self):
return 1 << 31
try:
len(X())
except OverflowError:
# 32-bit
MAXSIZE = int((1 << 31) - 1)
else:
# 64-bit
MAXSIZE = int((1 << 63) - 1)
del X
def _add_doc(func, doc):
"""Add documentation to a function."""
func.__doc__ = doc
def _import_module(name):
"""Import module, returning the module after the last dot."""
__import__(name)
return sys.modules[name]
class _LazyDescr(object):
def __init__(self, name):
self.name = name
def __get__(self, obj, tp):
result = self._resolve()
setattr(obj, self.name, result)
# This is a bit ugly, but it avoids running this again.
delattr(tp, self.name)
return result
class MovedModule(_LazyDescr):
def __init__(self, name, old, new=None):
super(MovedModule, self).__init__(name)
if PY3:
if new is None:
new = name
self.mod = new
else:
self.mod = old
def _resolve(self):
return _import_module(self.mod)
class MovedAttribute(_LazyDescr):
def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
super(MovedAttribute, self).__init__(name)
if PY3:
if new_mod is None:
new_mod = name
self.mod = new_mod
if new_attr is None:
if old_attr is None:
new_attr = name
else:
new_attr = old_attr
self.attr = new_attr
else:
self.mod = old_mod
if old_attr is None:
old_attr = name
self.attr = old_attr
def _resolve(self):
module = _import_module(self.mod)
return getattr(module, self.attr)
class _MovedItems(types.ModuleType):
"""Lazy loading of moved objects"""
_moved_attributes = [
MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
MovedAttribute("map", "itertools", "builtins", "imap", "map"),
MovedAttribute("reload_module", "__builtin__", "imp", "reload"),
MovedAttribute("reduce", "__builtin__", "functools"),
MovedAttribute("StringIO", "StringIO", "io"),
MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
MovedModule("builtins", "__builtin__"),
MovedModule("configparser", "ConfigParser"),
MovedModule("copyreg", "copy_reg"),
MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
MovedModule("http_cookies", "Cookie", "http.cookies"),
MovedModule("html_entities", "htmlentitydefs", "html.entities"),
MovedModule("html_parser", "HTMLParser", "html.parser"),
MovedModule("http_client", "httplib", "http.client"),
MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
MovedModule("cPickle", "cPickle", "pickle"),
MovedModule("queue", "Queue"),
MovedModule("reprlib", "repr"),
MovedModule("socketserver", "SocketServer"),
MovedModule("tkinter", "Tkinter"),
MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
MovedModule("tkinter_colorchooser", "tkColorChooser",
"tkinter.colorchooser"),
MovedModule("tkinter_commondialog", "tkCommonDialog",
"tkinter.commondialog"),
MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
MovedModule("tkinter_font", "tkFont", "tkinter.font"),
MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
"tkinter.simpledialog"),
MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
MovedModule("winreg", "_winreg"),
]
for attr in _moved_attributes:
setattr(_MovedItems, attr.name, attr)
del attr
moves = sys.modules["django.utils.six.moves"] = _MovedItems("moves")
def add_move(move):
"""Add an item to six.moves."""
setattr(_MovedItems, move.name, move)
def remove_move(name):
"""Remove item from six.moves."""
try:
delattr(_MovedItems, name)
except AttributeError:
try:
del moves.__dict__[name]
except KeyError:
raise AttributeError("no such move, %r" % (name,))
if PY3:
_meth_func = "__func__"
_meth_self = "__self__"
_func_code = "__code__"
_func_defaults = "__defaults__"
_iterkeys = "keys"
_itervalues = "values"
_iteritems = "items"
else:
_meth_func = "im_func"
_meth_self = "im_self"
_func_code = "func_code"
_func_defaults = "func_defaults"
_iterkeys = "iterkeys"
_itervalues = "itervalues"
_iteritems = "iteritems"
try:
advance_iterator = next
except NameError:
def advance_iterator(it):
return it.next()
next = advance_iterator
if PY3:
def get_unbound_function(unbound):
return unbound
Iterator = object
def callable(obj):
return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
else:
def get_unbound_function(unbound):
return unbound.im_func
class Iterator(object):
def next(self):
return type(self).__next__(self)
callable = callable
_add_doc(get_unbound_function,
"""Get the function out of a possibly unbound function""")
get_method_function = operator.attrgetter(_meth_func)
get_method_self = operator.attrgetter(_meth_self)
get_function_code = operator.attrgetter(_func_code)
get_function_defaults = operator.attrgetter(_func_defaults)
def iterkeys(d):
"""Return an iterator over the keys of a dictionary."""
return iter(getattr(d, _iterkeys)())
def itervalues(d):
"""Return an iterator over the values of a dictionary."""
return iter(getattr(d, _itervalues)())
def iteritems(d):
"""Return an iterator over the (key, value) pairs of a dictionary."""
return iter(getattr(d, _iteritems)())
if PY3:
def b(s):
return s.encode("latin-1")
def u(s):
return s
if sys.version_info[1] <= 1:
def int2byte(i):
return bytes((i,))
else:
# This is about 2x faster than the implementation above on 3.2+
int2byte = operator.methodcaller("to_bytes", 1, "big")
import io
StringIO = io.StringIO
BytesIO = io.BytesIO
else:
def b(s):
return s
def u(s):
return unicode(s, "unicode_escape")
int2byte = chr
import StringIO
StringIO = BytesIO = StringIO.StringIO
_add_doc(b, """Byte literal""")
_add_doc(u, """Text literal""")
if PY3:
import builtins
exec_ = getattr(builtins, "exec")
def reraise(tp, value, tb=None):
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
print_ = getattr(builtins, "print")
del builtins
else:
def exec_(code, globs=None, locs=None):
"""Execute code in a namespace."""
if globs is None:
frame = sys._getframe(1)
globs = frame.f_globals
if locs is None:
locs = frame.f_locals
del frame
elif locs is None:
locs = globs
exec("""exec code in globs, locs""")
exec_("""def reraise(tp, value, tb=None):
raise tp, value, tb
""")
def print_(*args, **kwargs):
"""The new-style print function."""
fp = kwargs.pop("file", sys.stdout)
if fp is None:
return
def write(data):
if not isinstance(data, basestring):
data = str(data)
fp.write(data)
want_unicode = False
sep = kwargs.pop("sep", None)
if sep is not None:
if isinstance(sep, unicode):
want_unicode = True
elif not isinstance(sep, str):
raise TypeError("sep must be None or a string")
end = kwargs.pop("end", None)
if end is not None:
if isinstance(end, unicode):
want_unicode = True
elif not isinstance(end, str):
raise TypeError("end must be None or a string")
if kwargs:
raise TypeError("invalid keyword arguments to print()")
if not want_unicode:
for arg in args:
if isinstance(arg, unicode):
want_unicode = True
break
if want_unicode:
newline = unicode("\n")
space = unicode(" ")
else:
newline = "\n"
space = " "
if sep is None:
sep = space
if end is None:
end = newline
for i, arg in enumerate(args):
if i:
write(sep)
write(arg)
write(end)
_add_doc(reraise, """Reraise an exception.""")
def with_metaclass(meta, base=object):
"""Create a base class with a metaclass."""
return meta("NewBase", (base,), {})
### Additional customizations for Django ###
if PY3:
_iterlists = "lists"
_assertRaisesRegex = "assertRaisesRegex"
else:
_iterlists = "iterlists"
_assertRaisesRegex = "assertRaisesRegexp"
def iterlists(d):
"""Return an iterator over the values of a MultiValueDict."""
return getattr(d, _iterlists)()
def assertRaisesRegex(self, *args, **kwargs):
return getattr(self, _assertRaisesRegex)(*args, **kwargs)
add_move(MovedModule("_dummy_thread", "dummy_thread"))
add_move(MovedModule("_thread", "thread"))

View File

@ -10,15 +10,19 @@ from __future__ import unicode_literals
def is_informational(code): def is_informational(code):
return code >= 100 and code <= 199 return code >= 100 and code <= 199
def is_success(code): def is_success(code):
return code >= 200 and code <= 299 return code >= 200 and code <= 299
def is_redirect(code): def is_redirect(code):
return code >= 300 and code <= 399 return code >= 300 and code <= 399
def is_client_error(code): def is_client_error(code):
return code >= 400 and code <= 499 return code >= 400 and code <= 499
def is_server_error(code): def is_server_error(code):
return code >= 500 and code <= 599 return code >= 500 and code <= 599

View File

@ -1,4 +1,5 @@
{% load url from future %} {% load url from future %}
{% load staticfiles %}
{% load rest_framework %} {% load rest_framework %}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>

View File

@ -1,5 +1,6 @@
{% extends "rest_framework/base.html" %} {% extends "rest_framework/base.html" %}
{% load url from future %} {% load url from future %}
{% load staticfiles %}
{% load rest_framework %} {% load rest_framework %}
{% block body %} {% block body %}

View File

@ -2,98 +2,17 @@ from __future__ import unicode_literals, absolute_import
from django import template from django import template
from django.core.urlresolvers import reverse, NoReverseMatch from django.core.urlresolvers import reverse, NoReverseMatch
from django.http import QueryDict from django.http import QueryDict
from django.utils import six
from django.utils.encoding import iri_to_uri from django.utils.encoding import iri_to_uri
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import SafeData, mark_safe from django.utils.safestring import SafeData, mark_safe
from rest_framework.compat import urlparse, force_text, six, smart_urlquote from django.utils.html import smart_urlquote
from rest_framework.compat import urlparse, force_text
import re import re
register = template.Library() register = template.Library()
# Note we don't use 'load staticfiles', because we need a 1.3 compatible
# version, so instead we include the `static` template tag ourselves.
# When 1.3 becomes unsupported by REST framework, we can instead start to
# use the {% load staticfiles %} tag, remove the following code,
# and add a dependency that `django.contrib.staticfiles` must be installed.
# Note: We can't put this into the `compat` module because the compat import
# from rest_framework.compat import ...
# conflicts with this rest_framework template tag module.
try: # Django 1.5+
from django.contrib.staticfiles.templatetags.staticfiles import StaticFilesNode
@register.tag('static')
def do_static(parser, token):
return StaticFilesNode.handle_token(parser, token)
except ImportError:
try: # Django 1.4
from django.contrib.staticfiles.storage import staticfiles_storage
@register.simple_tag
def static(path):
"""
A template tag that returns the URL to a file
using staticfiles' storage backend
"""
return staticfiles_storage.url(path)
except ImportError: # Django 1.3
from urlparse import urljoin
from django import template
from django.templatetags.static import PrefixNode
class StaticNode(template.Node):
def __init__(self, varname=None, path=None):
if path is None:
raise template.TemplateSyntaxError(
"Static template nodes must be given a path to return.")
self.path = path
self.varname = varname
def url(self, context):
path = self.path.resolve(context)
return self.handle_simple(path)
def render(self, context):
url = self.url(context)
if self.varname is None:
return url
context[self.varname] = url
return ''
@classmethod
def handle_simple(cls, path):
return urljoin(PrefixNode.handle_simple("STATIC_URL"), path)
@classmethod
def handle_token(cls, parser, token):
"""
Class method to parse prefix node and return a Node.
"""
bits = token.split_contents()
if len(bits) < 2:
raise template.TemplateSyntaxError(
"'%s' takes at least one argument (path to file)" % bits[0])
path = parser.compile_filter(bits[1])
if len(bits) >= 2 and bits[-2] == 'as':
varname = bits[3]
else:
varname = None
return cls(varname, path)
@register.tag('static')
def do_static_13(parser, token):
return StaticNode.handle_token(parser, token)
def replace_query_param(url, key, val): def replace_query_param(url, key, val):
""" """
Given a URL and a key/val pair, set or replace an item in the query Given a URL and a key/val pair, set or replace an item in the query
@ -234,8 +153,10 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
middle = middle[len(opening):] middle = middle[len(opening):]
lead = lead + opening lead = lead + opening
# Keep parentheses at the end only if they're balanced. # Keep parentheses at the end only if they're balanced.
if (middle.endswith(closing) if (
and middle.count(closing) == middle.count(opening) + 1): middle.endswith(closing)
and middle.count(closing) == middle.count(opening) + 1
):
middle = middle[:-len(closing)] middle = middle[:-len(closing)]
trail = closing + trail trail = closing + trail
@ -246,7 +167,7 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
url = smart_urlquote_wrapper(middle) url = smart_urlquote_wrapper(middle)
elif simple_url_2_re.match(middle): elif simple_url_2_re.match(middle):
url = smart_urlquote_wrapper('http://%s' % middle) url = smart_urlquote_wrapper('http://%s' % middle)
elif not ':' in middle and simple_email_re.match(middle): elif ':' not in middle and simple_email_re.match(middle):
local, domain = middle.rsplit('@', 1) local, domain = middle.rsplit('@', 1)
try: try:
domain = domain.encode('idna').decode('ascii') domain = domain.encode('idna').decode('ascii')

View File

@ -8,10 +8,11 @@ from django.conf import settings
from django.test.client import Client as DjangoClient from django.test.client import Client as DjangoClient
from django.test.client import ClientHandler from django.test.client import ClientHandler
from django.test import testcases from django.test import testcases
from django.utils import six
from django.utils.http import urlencode from django.utils.http import urlencode
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.compat import RequestFactory as DjangoRequestFactory from rest_framework.compat import RequestFactory as DjangoRequestFactory
from rest_framework.compat import force_bytes_or_smart_bytes, six from rest_framework.compat import force_bytes_or_smart_bytes
def force_authenticate(request, user=None, token=None): def force_authenticate(request, user=None, token=None):
@ -49,9 +50,10 @@ class APIRequestFactory(DjangoRequestFactory):
else: else:
format = format or self.default_format format = format or self.default_format
assert format in self.renderer_classes, ("Invalid format '{0}'. " assert format in self.renderer_classes, (
"Available formats are {1}. Set TEST_REQUEST_RENDERER_CLASSES " "Invalid format '{0}'. Available formats are {1}. "
"to enable extra request formats.".format( "Set TEST_REQUEST_RENDERER_CLASSES to enable "
"extra request formats.".format(
format, format,
', '.join(["'" + fmt + "'" for fmt in self.renderer_classes.keys()]) ', '.join(["'" + fmt + "'" for fmt in self.renderer_classes.keys()])
) )

View File

@ -1,73 +0,0 @@
from __future__ import unicode_literals
from django.test import TestCase
from rest_framework.compat import patterns, url
from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework.views import APIView
class Root(APIView):
pass
class ResourceRoot(APIView):
pass
class ResourceInstance(APIView):
pass
class NestedResourceRoot(APIView):
pass
class NestedResourceInstance(APIView):
pass
urlpatterns = patterns('',
url(r'^$', Root.as_view()),
url(r'^resource/$', ResourceRoot.as_view()),
url(r'^resource/(?P<key>[0-9]+)$', ResourceInstance.as_view()),
url(r'^resource/(?P<key>[0-9]+)/$', NestedResourceRoot.as_view()),
url(r'^resource/(?P<key>[0-9]+)/(?P<other>[A-Za-z]+)$', NestedResourceInstance.as_view()),
)
class BreadcrumbTests(TestCase):
"""Tests the breadcrumb functionality used by the HTML renderer."""
urls = 'rest_framework.tests.test_breadcrumbs'
def test_root_breadcrumbs(self):
url = '/'
self.assertEqual(get_breadcrumbs(url), [('Root', '/')])
def test_resource_root_breadcrumbs(self):
url = '/resource/'
self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
('Resource Root', '/resource/')])
def test_resource_instance_breadcrumbs(self):
url = '/resource/123'
self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
('Resource Root', '/resource/'),
('Resource Instance', '/resource/123')])
def test_nested_resource_breadcrumbs(self):
url = '/resource/123/'
self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
('Resource Root', '/resource/'),
('Resource Instance', '/resource/123'),
('Nested Resource Root', '/resource/123/')])
def test_nested_resource_instance_breadcrumbs(self):
url = '/resource/123/abc'
self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
('Resource Root', '/resource/'),
('Resource Instance', '/resource/123'),
('Nested Resource Root', '/resource/123/'),
('Nested Resource Instance', '/resource/123/abc')])
def test_broken_url_breadcrumbs_handled_gracefully(self):
url = '/foobar'
self.assertEqual(get_breadcrumbs(url), [('Root', '/')])

View File

@ -1,16 +0,0 @@
"""
Force import of all modules in this package in order to get the standard test
runner to pick up the tests. Yowzers.
"""
from __future__ import unicode_literals
import os
import django
modules = [filename.rsplit('.', 1)[0]
for filename in os.listdir(os.path.dirname(__file__))
if filename.endswith('.py') and not filename.startswith('_')]
__test__ = dict()
if django.VERSION < (1, 6):
for module in modules:
exec("from rest_framework.tests.%s import *" % module)

View File

@ -18,6 +18,25 @@ class BaseThrottle(object):
""" """
raise NotImplementedError('.allow_request() must be overridden') raise NotImplementedError('.allow_request() must be overridden')
def get_ident(self, request):
"""
Identify the machine making the request by parsing HTTP_X_FORWARDED_FOR
if present and number of proxies is > 0. If not use all of
HTTP_X_FORWARDED_FOR if it is available, if not use REMOTE_ADDR.
"""
xff = request.META.get('HTTP_X_FORWARDED_FOR')
remote_addr = request.META.get('REMOTE_ADDR')
num_proxies = api_settings.NUM_PROXIES
if num_proxies is not None:
if num_proxies == 0 or xff is None:
return remote_addr
addrs = xff.split(',')
client_addr = addrs[-min(num_proxies, len(xff))]
return client_addr.strip()
return xff if xff else remote_addr
def wait(self): def wait(self):
""" """
Optionally, return a recommended number of seconds to wait before Optionally, return a recommended number of seconds to wait before
@ -162,7 +181,7 @@ class AnonRateThrottle(SimpleRateThrottle):
return self.cache_format % { return self.cache_format % {
'scope': self.scope, 'scope': self.scope,
'ident': ident 'ident': self.get_ident(request)
} }
@ -180,7 +199,7 @@ class UserRateThrottle(SimpleRateThrottle):
if request.user.is_authenticated(): if request.user.is_authenticated():
ident = request.user.id ident = request.user.id
else: else:
ident = request.META.get('REMOTE_ADDR', None) ident = self.get_ident(request)
return self.cache_format % { return self.cache_format % {
'scope': self.scope, 'scope': self.scope,
@ -228,7 +247,7 @@ class ScopedRateThrottle(SimpleRateThrottle):
if request.user.is_authenticated(): if request.user.is_authenticated():
ident = request.user.id ident = request.user.id
else: else:
ident = request.META.get('REMOTE_ADDR', None) ident = self.get_ident(request)
return self.cache_format % { return self.cache_format % {
'scope': self.scope, 'scope': self.scope,

View File

@ -1,6 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.urls import url, include
from django.core.urlresolvers import RegexURLResolver from django.core.urlresolvers import RegexURLResolver
from rest_framework.compat import url, include
from rest_framework.settings import api_settings from rest_framework.settings import api_settings

View File

@ -8,17 +8,19 @@ your API requires authentication:
... ...
url(r'^auth', include('rest_framework.urls', namespace='rest_framework')) url(r'^auth', include('rest_framework.urls', namespace='rest_framework'))
) )
The urls must be namespaced as 'rest_framework', and you should make sure The urls must be namespaced as 'rest_framework', and you should make sure
your authentication settings include `SessionAuthentication`. your authentication settings include `SessionAuthentication`.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework.compat import patterns, url from django.conf.urls import patterns, url
from django.contrib.auth import views
template_name = {'template_name': 'rest_framework/login.html'} template_name = {'template_name': 'rest_framework/login.html'}
urlpatterns = patterns('django.contrib.auth.views', urlpatterns = patterns(
url(r'^login/$', 'login', template_name, name='login'), '',
url(r'^logout/$', 'logout', template_name, name='logout'), url(r'^login/$', views.login, template_name, name='login'),
url(r'^logout/$', views.logout, template_name, name='logout')
) )

View File

@ -2,10 +2,11 @@
Helper classes for parsers. Helper classes for parsers.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils import timezone
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from django.utils.functional import Promise from django.utils.functional import Promise
from rest_framework.compat import timezone, force_text from rest_framework.compat import force_text
from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
import datetime import datetime
import decimal import decimal
@ -97,14 +98,23 @@ else:
node.flow_style = best_style node.flow_style = best_style
return node return node
SafeDumper.add_representer(decimal.Decimal, SafeDumper.add_representer(
SafeDumper.represent_decimal) decimal.Decimal,
SafeDumper.represent_decimal
SafeDumper.add_representer(SortedDict, )
yaml.representer.SafeRepresenter.represent_dict) SafeDumper.add_representer(
SafeDumper.add_representer(DictWithMetadata, SortedDict,
yaml.representer.SafeRepresenter.represent_dict) yaml.representer.SafeRepresenter.represent_dict
SafeDumper.add_representer(SortedDictWithMetadata, )
yaml.representer.SafeRepresenter.represent_dict) SafeDumper.add_representer(
SafeDumper.add_representer(types.GeneratorType, DictWithMetadata,
yaml.representer.SafeRepresenter.represent_list) yaml.representer.SafeRepresenter.represent_dict
)
SafeDumper.add_representer(
SortedDictWithMetadata,
yaml.representer.SafeRepresenter.represent_dict
)
SafeDumper.add_representer(
types.GeneratorType,
yaml.representer.SafeRepresenter.represent_list
)

View File

@ -6,8 +6,6 @@ from __future__ import unicode_literals
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from rest_framework.compat import apply_markdown from rest_framework.compat import apply_markdown
from rest_framework.settings import api_settings
from textwrap import dedent
import re import re
@ -40,6 +38,7 @@ def dedent(content):
return content.strip() return content.strip()
def camelcase_to_spaces(content): def camelcase_to_spaces(content):
""" """
Translate 'CamelCaseNames' to 'Camel Case Names'. Translate 'CamelCaseNames' to 'Camel Case Names'.
@ -49,6 +48,7 @@ def camelcase_to_spaces(content):
content = re.sub(camelcase_boundry, ' \\1', content).strip() content = re.sub(camelcase_boundry, ' \\1', content).strip()
return ' '.join(content.split('_')).title() return ' '.join(content.split('_')).title()
def markup_description(description): def markup_description(description):
""" """
Apply HTML markup to the given description. Apply HTML markup to the given description.

View File

@ -57,7 +57,7 @@ class _MediaType(object):
if key != 'q' and other.params.get(key, None) != self.params.get(key, None): if key != 'q' and other.params.get(key, None) != self.params.get(key, None):
return False return False
if self.sub_type != '*' and other.sub_type != '*' and other.sub_type != self.sub_type: if self.sub_type != '*' and other.sub_type != '*' and other.sub_type != self.sub_type:
return False return False
if self.main_type != '*' and other.main_type != '*' and other.main_type != self.main_type: if self.main_type != '*' and other.main_type != '*' and other.main_type != self.main_type:
@ -79,7 +79,7 @@ class _MediaType(object):
return 3 return 3
def __str__(self): def __str__(self):
return unicode(self).encode('utf-8') return self.__unicode__().encode('utf-8')
def __unicode__(self): def __unicode__(self):
ret = "%s/%s" % (self.main_type, self.sub_type) ret = "%s/%s" % (self.main_type, self.sub_type)

View File

@ -31,6 +31,7 @@ def get_view_name(view_cls, suffix=None):
return name return name
def get_view_description(view_cls, html=False): def get_view_description(view_cls, html=False):
""" """
Given a view class, return a textual description to represent the view. Given a view class, return a textual description to represent the view.
@ -119,7 +120,6 @@ class APIView(View):
headers['Vary'] = 'Accept' headers['Vary'] = 'Accept'
return headers return headers
def http_method_not_allowed(self, request, *args, **kwargs): def http_method_not_allowed(self, request, *args, **kwargs):
""" """
If `request.method` does not correspond to a handler method, If `request.method` does not correspond to a handler method,

View File

@ -127,11 +127,11 @@ class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
class ModelViewSet(mixins.CreateModelMixin, class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.UpdateModelMixin, mixins.UpdateModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet): GenericViewSet):
""" """
A viewset that provides default `create()`, `retrieve()`, `update()`, A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions. `partial_update()`, `destroy()` and `list()` actions.

86
runtests.py Executable file
View File

@ -0,0 +1,86 @@
#! /usr/bin/env python
from __future__ import print_function
import pytest
import sys
import os
import subprocess
PYTEST_ARGS = {
'default': ['tests'],
'fast': ['tests', '-q'],
}
FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501']
sys.path.append(os.path.dirname(__file__))
def exit_on_failure(ret, message=None):
if ret:
sys.exit(ret)
def flake8_main(args):
print('Running flake8 code linting')
ret = subprocess.call(['flake8'] + args)
print('flake8 failed' if ret else 'flake8 passed')
return ret
def split_class_and_function(string):
class_string, function_string = string.split('.', 1)
return "%s and %s" % (class_string, function_string)
def is_function(string):
# `True` if it looks like a test function is included in the string.
return string.startswith('test_') or '.test_' in string
def is_class(string):
# `True` if first character is uppercase - assume it's a class name.
return string[0] == string[0].upper()
if __name__ == "__main__":
try:
sys.argv.remove('--nolint')
except ValueError:
run_flake8 = True
else:
run_flake8 = False
try:
sys.argv.remove('--lintonly')
except ValueError:
run_tests = True
else:
run_tests = False
try:
sys.argv.remove('--fast')
except ValueError:
style = 'default'
else:
style = 'fast'
run_flake8 = False
if len(sys.argv) > 1:
pytest_args = sys.argv[1:]
first_arg = pytest_args[0]
if first_arg.startswith('-'):
# `runtests.py [flags]`
pytest_args = ['tests'] + pytest_args
elif is_class(first_arg) and is_function(first_arg):
# `runtests.py TestCase.test_function [flags]`
expression = split_class_and_function(first_arg)
pytest_args = ['tests', '-k', expression] + pytest_args[1:]
elif is_class(first_arg) or is_function(first_arg):
# `runtests.py TestCase [flags]`
# `runtests.py test_function [flags]`
pytest_args = ['tests', '-k', pytest_args[0]] + pytest_args[1:]
else:
pytest_args = PYTEST_ARGS[style]
if run_tests:
exit_on_failure(pytest.main(pytest_args))
if run_flake8:
exit_on_failure(flake8_main(FLAKE8_ARGS))

View File

@ -2,11 +2,26 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from setuptools import setup from setuptools import setup
from setuptools.command.test import test as TestCommand
import re import re
import os import os
import sys import sys
# This command has been borrowed from
# https://github.com/getsentry/sentry/blob/master/setup.py
class PyTest(TestCommand):
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = ['tests']
self.test_suite = True
def run_tests(self):
import pytest
errno = pytest.main(self.test_args)
sys.exit(errno)
def get_version(package): def get_version(package):
""" """
Return package version as listed in `__version__` in `init.py`. Return package version as listed in `__version__` in `init.py`.
@ -62,7 +77,7 @@ setup(
author_email='tom@tomchristie.com', # SEE NOTE BELOW (*) author_email='tom@tomchristie.com', # SEE NOTE BELOW (*)
packages=get_packages('rest_framework'), packages=get_packages('rest_framework'),
package_data=get_package_data('rest_framework'), package_data=get_package_data('rest_framework'),
test_suite='rest_framework.runtests.runtests.main', cmdclass={'test': PyTest},
install_requires=[], install_requires=[],
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',

View File

@ -1,6 +1,6 @@
from django.db import models from django.db import models
from rest_framework.tests.users.models import User from tests.users.models import User
class Account(models.Model): class Account(models.Model):

View File

@ -1,7 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from rest_framework.tests.accounts.models import Account from tests.accounts.models import Account
from rest_framework.tests.users.serializers import UserSerializer from tests.users.serializers import UserSerializer
class AccountSerializer(serializers.ModelSerializer): class AccountSerializer(serializers.ModelSerializer):

88
tests/conftest.py Normal file
View File

@ -0,0 +1,88 @@
def pytest_configure():
from django.conf import settings
settings.configure(
DEBUG_PROPAGATE_EXCEPTIONS=True,
DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:'}},
SECRET_KEY='not very secret in tests',
USE_I18N=True,
USE_L10N=True,
STATIC_URL='/static/',
ROOT_URLCONF='tests.urls',
TEMPLATE_LOADERS=(
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
),
MIDDLEWARE_CLASSES=(
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
),
INSTALLED_APPS=(
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'tests',
'tests.accounts',
'tests.records',
'tests.users',
),
PASSWORD_HASHERS=(
'django.contrib.auth.hashers.SHA1PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
'django.contrib.auth.hashers.MD5PasswordHasher',
'django.contrib.auth.hashers.CryptPasswordHasher',
),
)
try:
import oauth_provider # NOQA
import oauth2 # NOQA
except ImportError:
pass
else:
settings.INSTALLED_APPS += (
'oauth_provider',
)
try:
import provider # NOQA
except ImportError:
pass
else:
settings.INSTALLED_APPS += (
'provider',
'provider.oauth2',
)
# guardian is optional
try:
import guardian # NOQA
except ImportError:
pass
else:
settings.ANONYMOUS_USER_ID = -1
settings.AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'guardian.backends.ObjectPermissionBackend',
)
settings.INSTALLED_APPS += (
'guardian',
)
try:
import django
django.setup()
except AttributeError:
pass

View File

@ -60,6 +60,18 @@ class ReadOnlyManyToManyModel(RESTFrameworkModel):
rel = models.ManyToManyField(Anchor) rel = models.ManyToManyField(Anchor)
class BaseFilterableItem(RESTFrameworkModel):
text = models.CharField(max_length=100)
class Meta:
abstract = True
class FilterableItem(BaseFilterableItem):
decimal = models.DecimalField(max_digits=4, decimal_places=2)
date = models.DateField()
# Model for regression test for #285 # Model for regression test for #285
class Comment(RESTFrameworkModel): class Comment(RESTFrameworkModel):
@ -172,10 +184,3 @@ class NullableOneToOneSource(RESTFrameworkModel):
class BasicModelSerializer(serializers.ModelSerializer): class BasicModelSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = BasicModel model = BasicModel
# Models to test filters
class FilterableItem(models.Model):
text = models.CharField(max_length=100)
decimal = models.DecimalField(max_digits=4, decimal_places=2)
date = models.DateField()

View File

@ -1,6 +1,5 @@
from rest_framework import serializers from rest_framework import serializers
from tests.models import NullableForeignKeySource
from rest_framework.tests.models import NullableForeignKeySource
class NullableFKSourceSerializer(serializers.ModelSerializer): class NullableFKSourceSerializer(serializers.ModelSerializer):

View File

@ -68,7 +68,6 @@ SECRET_KEY = 'u@x-aj9(hoh#rb-^ymf#g2jx_hp0vj7u5#b@ag1n^seu9e!%cy'
TEMPLATE_LOADERS = ( TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader', 'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader', 'django.template.loaders.app_directories.Loader',
# 'django.template.loaders.eggs.Loader',
) )
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
@ -79,7 +78,7 @@ MIDDLEWARE_CLASSES = (
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
) )
ROOT_URLCONF = 'urls' ROOT_URLCONF = 'tests.urls'
TEMPLATE_DIRS = ( TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
@ -93,22 +92,19 @@ INSTALLED_APPS = (
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.messages', 'django.contrib.messages',
# Uncomment the next line to enable the admin: 'django.contrib.staticfiles',
# 'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'rest_framework.tests', 'tests',
'rest_framework.tests.accounts', 'tests.accounts',
'rest_framework.tests.records', 'tests.records',
'rest_framework.tests.users', 'tests.users',
) )
# OAuth is optional and won't work if there is no oauth_provider & oauth2 # OAuth is optional and won't work if there is no oauth_provider & oauth2
try: try:
import oauth_provider import oauth_provider # NOQA
import oauth2 import oauth2 # NOQA
except ImportError: except ImportError:
pass pass
else: else:
@ -117,7 +113,7 @@ else:
) )
try: try:
import provider import provider # NOQA
except ImportError: except ImportError:
pass pass
else: else:
@ -128,13 +124,13 @@ else:
# guardian is optional # guardian is optional
try: try:
import guardian import guardian # NOQA
except ImportError: except ImportError:
pass pass
else: else:
ANONYMOUS_USER_ID = -1 ANONYMOUS_USER_ID = -1
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', # default 'django.contrib.auth.backends.ModelBackend', # default
'guardian.backends.ObjectPermissionBackend', 'guardian.backends.ObjectPermissionBackend',
) )
INSTALLED_APPS += ( INSTALLED_APPS += (

View File

@ -1,8 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.urls import patterns, url, include
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.http import HttpResponse from django.http import HttpResponse
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import six, unittest
from django.utils.http import urlencode from django.utils.http import urlencode
from rest_framework import HTTP_HEADER_ENCODING from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import exceptions from rest_framework import exceptions
@ -19,7 +20,6 @@ from rest_framework.authentication import (
OAuth2Authentication OAuth2Authentication
) )
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from rest_framework.compat import patterns, url, include, six
from rest_framework.compat import oauth2_provider, oauth2_provider_scope from rest_framework.compat import oauth2_provider, oauth2_provider_scope
from rest_framework.compat import oauth, oauth_provider from rest_framework.compat import oauth, oauth_provider
from rest_framework.test import APIRequestFactory, APIClient from rest_framework.test import APIRequestFactory, APIClient
@ -44,32 +44,45 @@ class MockView(APIView):
return HttpResponse({'a': 1, 'b': 2, 'c': 3}) return HttpResponse({'a': 1, 'b': 2, 'c': 3})
urlpatterns = patterns('', urlpatterns = patterns(
'',
(r'^session/$', MockView.as_view(authentication_classes=[SessionAuthentication])), (r'^session/$', MockView.as_view(authentication_classes=[SessionAuthentication])),
(r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])), (r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])),
(r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])), (r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])),
(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'), (r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
(r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication])), (r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication])),
(r'^oauth-with-scope/$', MockView.as_view(authentication_classes=[OAuthAuthentication], (
permission_classes=[permissions.TokenHasReadWriteScope])) r'^oauth-with-scope/$',
MockView.as_view(
authentication_classes=[OAuthAuthentication],
permission_classes=[permissions.TokenHasReadWriteScope]
)
)
) )
class OAuth2AuthenticationDebug(OAuth2Authentication): class OAuth2AuthenticationDebug(OAuth2Authentication):
allow_query_params_token = True allow_query_params_token = True
if oauth2_provider is not None: if oauth2_provider is not None:
urlpatterns += patterns('', urlpatterns += patterns(
'',
url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')), url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])), url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])),
url(r'^oauth2-test-debug/$', MockView.as_view(authentication_classes=[OAuth2AuthenticationDebug])), url(r'^oauth2-test-debug/$', MockView.as_view(authentication_classes=[OAuth2AuthenticationDebug])),
url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication], url(
permission_classes=[permissions.TokenHasReadWriteScope])), r'^oauth2-with-scope-test/$',
MockView.as_view(
authentication_classes=[OAuth2Authentication],
permission_classes=[permissions.TokenHasReadWriteScope]
)
)
) )
class BasicAuthTests(TestCase): class BasicAuthTests(TestCase):
"""Basic authentication""" """Basic authentication"""
urls = 'rest_framework.tests.test_authentication' urls = 'tests.test_authentication'
def setUp(self): def setUp(self):
self.csrf_client = APIClient(enforce_csrf_checks=True) self.csrf_client = APIClient(enforce_csrf_checks=True)
@ -108,7 +121,7 @@ class BasicAuthTests(TestCase):
class SessionAuthTests(TestCase): class SessionAuthTests(TestCase):
"""User session authentication""" """User session authentication"""
urls = 'rest_framework.tests.test_authentication' urls = 'tests.test_authentication'
def setUp(self): def setUp(self):
self.csrf_client = APIClient(enforce_csrf_checks=True) self.csrf_client = APIClient(enforce_csrf_checks=True)
@ -155,7 +168,7 @@ class SessionAuthTests(TestCase):
class TokenAuthTests(TestCase): class TokenAuthTests(TestCase):
"""Token authentication""" """Token authentication"""
urls = 'rest_framework.tests.test_authentication' urls = 'tests.test_authentication'
def setUp(self): def setUp(self):
self.csrf_client = APIClient(enforce_csrf_checks=True) self.csrf_client = APIClient(enforce_csrf_checks=True)
@ -255,7 +268,7 @@ class IncorrectCredentialsTests(TestCase):
class OAuthTests(TestCase): class OAuthTests(TestCase):
"""OAuth 1.0a authentication""" """OAuth 1.0a authentication"""
urls = 'rest_framework.tests.test_authentication' urls = 'tests.test_authentication'
def setUp(self): def setUp(self):
# these imports are here because oauth is optional and hiding them in try..except block or compat # these imports are here because oauth is optional and hiding them in try..except block or compat
@ -277,12 +290,16 @@ class OAuthTests(TestCase):
self.TOKEN_KEY = "token_key" self.TOKEN_KEY = "token_key"
self.TOKEN_SECRET = "token_secret" self.TOKEN_SECRET = "token_secret"
self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET, self.consumer = Consumer.objects.create(
name='example', user=self.user, status=self.consts.ACCEPTED) key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET,
name='example', user=self.user, status=self.consts.ACCEPTED
)
self.scope = Scope.objects.create(name="resource name", url="api/") self.scope = Scope.objects.create(name="resource name", url="api/")
self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, scope=self.scope, self.token = OAuthToken.objects.create(
token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET, is_approved=True user=self.user, consumer=self.consumer, scope=self.scope,
token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET,
is_approved=True
) )
def _create_authorization_header(self): def _create_authorization_header(self):
@ -485,7 +502,7 @@ class OAuthTests(TestCase):
class OAuth2Tests(TestCase): class OAuth2Tests(TestCase):
"""OAuth 2.0 authentication""" """OAuth 2.0 authentication"""
urls = 'rest_framework.tests.test_authentication' urls = 'tests.test_authentication'
def setUp(self): def setUp(self):
self.csrf_client = APIClient(enforce_csrf_checks=True) self.csrf_client = APIClient(enforce_csrf_checks=True)
@ -500,24 +517,24 @@ class OAuth2Tests(TestCase):
self.REFRESH_TOKEN = "refresh_token" self.REFRESH_TOKEN = "refresh_token"
self.oauth2_client = oauth2_provider.oauth2.models.Client.objects.create( self.oauth2_client = oauth2_provider.oauth2.models.Client.objects.create(
client_id=self.CLIENT_ID, client_id=self.CLIENT_ID,
client_secret=self.CLIENT_SECRET, client_secret=self.CLIENT_SECRET,
redirect_uri='', redirect_uri='',
client_type=0, client_type=0,
name='example', name='example',
user=None, user=None,
) )
self.access_token = oauth2_provider.oauth2.models.AccessToken.objects.create( self.access_token = oauth2_provider.oauth2.models.AccessToken.objects.create(
token=self.ACCESS_TOKEN, token=self.ACCESS_TOKEN,
client=self.oauth2_client, client=self.oauth2_client,
user=self.user, user=self.user,
) )
self.refresh_token = oauth2_provider.oauth2.models.RefreshToken.objects.create( self.refresh_token = oauth2_provider.oauth2.models.RefreshToken.objects.create(
user=self.user, user=self.user,
access_token=self.access_token, access_token=self.access_token,
client=self.oauth2_client client=self.oauth2_client
) )
def _create_authorization_header(self, token=None): def _create_authorization_header(self, token=None):
return "Bearer {0}".format(token or self.access_token.token) return "Bearer {0}".format(token or self.access_token.token)
@ -568,8 +585,10 @@ class OAuth2Tests(TestCase):
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
def test_post_form_passing_auth_url_transport(self): def test_post_form_passing_auth_url_transport(self):
"""Ensure GETing form over OAuth with correct client credentials in form data succeed""" """Ensure GETing form over OAuth with correct client credentials in form data succeed"""
response = self.csrf_client.post('/oauth2-test/', response = self.csrf_client.post(
data={'access_token': self.access_token.token}) '/oauth2-test/',
data={'access_token': self.access_token.token}
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')

100
tests/test_breadcrumbs.py Normal file
View File

@ -0,0 +1,100 @@
from __future__ import unicode_literals
from django.conf.urls import patterns, url
from django.test import TestCase
from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework.views import APIView
class Root(APIView):
pass
class ResourceRoot(APIView):
pass
class ResourceInstance(APIView):
pass
class NestedResourceRoot(APIView):
pass
class NestedResourceInstance(APIView):
pass
urlpatterns = patterns(
'',
url(r'^$', Root.as_view()),
url(r'^resource/$', ResourceRoot.as_view()),
url(r'^resource/(?P<key>[0-9]+)$', ResourceInstance.as_view()),
url(r'^resource/(?P<key>[0-9]+)/$', NestedResourceRoot.as_view()),
url(r'^resource/(?P<key>[0-9]+)/(?P<other>[A-Za-z]+)$', NestedResourceInstance.as_view()),
)
class BreadcrumbTests(TestCase):
"""Tests the breadcrumb functionality used by the HTML renderer."""
urls = 'tests.test_breadcrumbs'
def test_root_breadcrumbs(self):
url = '/'
self.assertEqual(
get_breadcrumbs(url),
[('Root', '/')]
)
def test_resource_root_breadcrumbs(self):
url = '/resource/'
self.assertEqual(
get_breadcrumbs(url),
[
('Root', '/'),
('Resource Root', '/resource/')
]
)
def test_resource_instance_breadcrumbs(self):
url = '/resource/123'
self.assertEqual(
get_breadcrumbs(url),
[
('Root', '/'),
('Resource Root', '/resource/'),
('Resource Instance', '/resource/123')
]
)
def test_nested_resource_breadcrumbs(self):
url = '/resource/123/'
self.assertEqual(
get_breadcrumbs(url),
[
('Root', '/'),
('Resource Root', '/resource/'),
('Resource Instance', '/resource/123'),
('Nested Resource Root', '/resource/123/')
]
)
def test_nested_resource_instance_breadcrumbs(self):
url = '/resource/123/abc'
self.assertEqual(
get_breadcrumbs(url),
[
('Root', '/'),
('Resource Root', '/resource/'),
('Resource Instance', '/resource/123'),
('Nested Resource Root', '/resource/123/'),
('Nested Resource Instance', '/resource/123/abc')
]
)
def test_broken_url_breadcrumbs_handled_gracefully(self):
url = '/foobar'
self.assertEqual(
get_breadcrumbs(url),
[('Root', '/')]
)

View File

@ -4,8 +4,8 @@ from __future__ import unicode_literals
from django.test import TestCase from django.test import TestCase
from rest_framework.compat import apply_markdown, smart_text from rest_framework.compat import apply_markdown, smart_text
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.tests.description import ViewWithNonASCIICharactersInDocstring from .description import ViewWithNonASCIICharactersInDocstring
from rest_framework.tests.description import UTF8_TEST_DOCSTRING from .description import UTF8_TEST_DOCSTRING
# We check that docstrings get nicely un-indented. # We check that docstrings get nicely un-indented.
DESCRIPTION = """an example docstring DESCRIPTION = """an example docstring

View File

@ -12,7 +12,7 @@ from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from rest_framework import serializers from rest_framework import serializers
from rest_framework.tests.models import RESTFrameworkModel from tests.models import RESTFrameworkModel
class TimestampedModel(models.Model): class TimestampedModel(models.Model):
@ -648,7 +648,7 @@ class DecimalFieldTest(TestCase):
s = DecimalSerializer(data={'decimal_field': '123'}) s = DecimalSerializer(data={'decimal_field': '123'})
self.assertFalse(s.is_valid()) self.assertFalse(s.is_valid())
self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is less than or equal to 100.']}) self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is less than or equal to 100.']})
def test_raise_min_value(self): def test_raise_min_value(self):
""" """
@ -660,7 +660,7 @@ class DecimalFieldTest(TestCase):
s = DecimalSerializer(data={'decimal_field': '99'}) s = DecimalSerializer(data={'decimal_field': '99'})
self.assertFalse(s.is_valid()) self.assertFalse(s.is_valid())
self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is greater than or equal to 100.']}) self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is greater than or equal to 100.']})
def test_raise_max_digits(self): def test_raise_max_digits(self):
""" """
@ -672,7 +672,7 @@ class DecimalFieldTest(TestCase):
s = DecimalSerializer(data={'decimal_field': '123.456'}) s = DecimalSerializer(data={'decimal_field': '123.456'})
self.assertFalse(s.is_valid()) self.assertFalse(s.is_valid())
self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 5 digits in total.']}) self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 5 digits in total.']})
def test_raise_max_decimal_places(self): def test_raise_max_decimal_places(self):
""" """
@ -684,7 +684,7 @@ class DecimalFieldTest(TestCase):
s = DecimalSerializer(data={'decimal_field': '123.4567'}) s = DecimalSerializer(data={'decimal_field': '123.4567'})
self.assertFalse(s.is_valid()) self.assertFalse(s.is_valid())
self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 3 decimal places.']}) self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 3 decimal places.']})
def test_raise_max_whole_digits(self): def test_raise_max_whole_digits(self):
""" """
@ -696,7 +696,7 @@ class DecimalFieldTest(TestCase):
s = DecimalSerializer(data={'decimal_field': '12345.6'}) s = DecimalSerializer(data={'decimal_field': '12345.6'})
self.assertFalse(s.is_valid()) self.assertFalse(s.is_valid())
self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']}) self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
class ChoiceFieldTests(TestCase): class ChoiceFieldTests(TestCase):
@ -729,7 +729,7 @@ class ChoiceFieldTests(TestCase):
def test_invalid_choice_model(self): def test_invalid_choice_model(self):
s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'}) s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
self.assertFalse(s.is_valid()) self.assertFalse(s.is_valid())
self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']}) self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']})
self.assertEqual(s.data['choice'], '') self.assertEqual(s.data['choice'], '')
def test_empty_choice_model(self): def test_empty_choice_model(self):
@ -875,7 +875,7 @@ class SlugFieldTests(TestCase):
s = SlugFieldSerializer(data={'slug_field': 'a b'}) s = SlugFieldSerializer(data={'slug_field': 'a b'})
self.assertEqual(s.is_valid(), False) self.assertEqual(s.is_valid(), False)
self.assertEqual(s.errors, {'slug_field': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]}) self.assertEqual(s.errors, {'slug_field': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]})
class URLFieldTests(TestCase): class URLFieldTests(TestCase):

View File

@ -1,8 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.test import TestCase from django.test import TestCase
from django.utils import six
from rest_framework import serializers from rest_framework import serializers
from rest_framework.compat import BytesIO from rest_framework.compat import BytesIO
from rest_framework.compat import six
import datetime import datetime
@ -85,11 +85,8 @@ class FileSerializerTests(TestCase):
""" """
Validation should still function when no data dictionary is provided. Validation should still function when no data dictionary is provided.
""" """
now = datetime.datetime.now() uploaded_file = BytesIO(six.b('stuff'))
file = BytesIO(six.b('stuff')) uploaded_file.name = 'stuff.txt'
file.name = 'stuff.txt' uploaded_file.size = len(uploaded_file.getvalue())
file.size = len(file.getvalue()) serializer = UploadedFileSerializer(files={'file': uploaded_file})
uploaded_file = UploadedFile(file=file, created=now)
serializer = UploadedFileSerializer(files={'file': file})
self.assertFalse(serializer.is_valid()) self.assertFalse(serializer.is_valid())

View File

@ -5,12 +5,11 @@ from django.db import models
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import unittest
from django.conf.urls import patterns, url
from rest_framework import generics, serializers, status, filters from rest_framework import generics, serializers, status, filters
from rest_framework.compat import django_filters, patterns, url from rest_framework.compat import django_filters
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import BasicModel from .models import BaseFilterableItem, FilterableItem, BasicModel
from .models import FilterableItem
from .utils import temporary_setting from .utils import temporary_setting
factory = APIRequestFactory() factory = APIRequestFactory()
@ -56,6 +55,18 @@ if django_filters:
filter_class = SeveralFieldsFilter filter_class = SeveralFieldsFilter
filter_backends = (filters.DjangoFilterBackend,) filter_backends = (filters.DjangoFilterBackend,)
# These classes are used to test base model filter support
class BaseFilterableItemFilter(django_filters.FilterSet):
text = django_filters.CharFilter()
class Meta:
model = BaseFilterableItem
class BaseFilterableItemFilterRootView(generics.ListCreateAPIView):
model = FilterableItem
filter_class = BaseFilterableItemFilter
filter_backends = (filters.DjangoFilterBackend,)
# Regression test for #814 # Regression test for #814
class FilterableItemSerializer(serializers.ModelSerializer): class FilterableItemSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@ -75,7 +86,8 @@ if django_filters:
def get_queryset(self): def get_queryset(self):
return FilterableItem.objects.all() return FilterableItem.objects.all()
urlpatterns = patterns('', urlpatterns = patterns(
'',
url(r'^(?P<pk>\d+)/$', FilterClassDetailView.as_view(), name='detail-view'), url(r'^(?P<pk>\d+)/$', FilterClassDetailView.as_view(), name='detail-view'),
url(r'^$', FilterClassRootView.as_view(), name='root-view'), url(r'^$', FilterClassRootView.as_view(), name='root-view'),
url(r'^get-queryset/$', GetQuerysetView.as_view(), url(r'^get-queryset/$', GetQuerysetView.as_view(),
@ -226,6 +238,18 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
request = factory.get('/') request = factory.get('/')
self.assertRaises(AssertionError, view, request) self.assertRaises(AssertionError, view, request)
@unittest.skipUnless(django_filters, 'django-filter not installed')
def test_base_model_filter(self):
"""
The `get_filter_class` model checks should allow base model filters.
"""
view = BaseFilterableItemFilterRootView.as_view()
request = factory.get('/?text=aaa')
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
@unittest.skipUnless(django_filters, 'django-filter not installed') @unittest.skipUnless(django_filters, 'django-filter not installed')
def test_unknown_filter(self): def test_unknown_filter(self):
""" """
@ -243,7 +267,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase):
""" """
Integration tests for filtered detail views. Integration tests for filtered detail views.
""" """
urls = 'rest_framework.tests.test_filters' urls = 'tests.test_filters'
def _get_url(self, item): def _get_url(self, item):
return reverse('detail-view', kwargs=dict(pk=item.pk)) return reverse('detail-view', kwargs=dict(pk=item.pk))
@ -654,8 +678,8 @@ class SensitiveOrderingFilterTests(TestCase):
self.assertEqual( self.assertEqual(
response.data, response.data,
[ [
{'id': 1, username_field: 'userA'}, # PassB {'id': 1, username_field: 'userA'}, # PassB
{'id': 2, username_field: 'userB'}, # PassC {'id': 2, username_field: 'userB'}, # PassC
{'id': 3, username_field: 'userC'}, # PassA {'id': 3, username_field: 'userC'}, # PassA
] ]
) )

View File

@ -84,7 +84,7 @@ class TestGenericRelations(TestCase):
exclude = ('content_type', 'object_id') exclude = ('content_type', 'object_id')
class BookmarkSerializer(serializers.ModelSerializer): class BookmarkSerializer(serializers.ModelSerializer):
tags = TagSerializer() tags = TagSerializer(many=True)
class Meta: class Meta:
model = Bookmark model = Bookmark
@ -117,18 +117,18 @@ class TestGenericRelations(TestCase):
serializer = TagSerializer(Tag.objects.all(), many=True) serializer = TagSerializer(Tag.objects.all(), many=True)
expected = [ expected = [
{ {
'tag': 'django', 'tag': 'django',
'tagged_item': 'Bookmark: https://www.djangoproject.com/' 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
}, },
{ {
'tag': 'python', 'tag': 'python',
'tagged_item': 'Bookmark: https://www.djangoproject.com/' 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
}, },
{ {
'tag': 'reminder', 'tag': 'reminder',
'tagged_item': 'Note: Remember the milk' 'tagged_item': 'Note: Remember the milk'
} }
] ]
self.assertEqual(serializer.data, expected) self.assertEqual(serializer.data, expected)

View File

@ -2,11 +2,11 @@ from __future__ import unicode_literals
from django.db import models from django.db import models
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.test import TestCase from django.test import TestCase
from django.utils import six
from rest_framework import generics, renderers, serializers, status from rest_framework import generics, renderers, serializers, status
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel from tests.models import BasicModel, Comment, SlugBasedModel
from rest_framework.tests.models import ForeignKeySource, ForeignKeyTarget from tests.models import ForeignKeySource, ForeignKeyTarget
from rest_framework.compat import six
factory = APIRequestFactory() factory = APIRequestFactory()

View File

@ -1,15 +1,15 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.conf.urls import patterns, url
from django.http import Http404 from django.http import Http404
from django.test import TestCase from django.test import TestCase
from django.template import TemplateDoesNotExist, Template from django.template import TemplateDoesNotExist, Template
import django.template.loader from django.utils import six
from rest_framework import status from rest_framework import status
from rest_framework.compat import patterns, url
from rest_framework.decorators import api_view, renderer_classes from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import TemplateHTMLRenderer from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.compat import six import django.template.loader
@api_view(('GET',)) @api_view(('GET',))
@ -34,7 +34,8 @@ def not_found(request):
raise Http404() raise Http404()
urlpatterns = patterns('', urlpatterns = patterns(
'',
url(r'^$', example), url(r'^$', example),
url(r'^permission_denied$', permission_denied), url(r'^permission_denied$', permission_denied),
url(r'^not_found$', not_found), url(r'^not_found$', not_found),
@ -42,7 +43,7 @@ urlpatterns = patterns('',
class TemplateHTMLRendererTests(TestCase): class TemplateHTMLRendererTests(TestCase):
urls = 'rest_framework.tests.test_htmlrenderer' urls = 'tests.test_htmlrenderer'
def setUp(self): def setUp(self):
""" """
@ -82,7 +83,7 @@ class TemplateHTMLRendererTests(TestCase):
class TemplateHTMLRendererExceptionTests(TestCase): class TemplateHTMLRendererExceptionTests(TestCase):
urls = 'rest_framework.tests.test_htmlrenderer' urls = 'tests.test_htmlrenderer'
def setUp(self): def setUp(self):
""" """

View File

@ -2,10 +2,10 @@ from __future__ import unicode_literals
import json import json
from django.test import TestCase from django.test import TestCase
from rest_framework import generics, status, serializers from rest_framework import generics, status, serializers
from rest_framework.compat import patterns, url from django.conf.urls import patterns, url
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import ( from tests.models import (
Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment,
Album, Photo, OptionalRelationModel Album, Photo, OptionalRelationModel
) )
@ -25,7 +25,7 @@ class BlogPostCommentSerializer(serializers.ModelSerializer):
class PhotoSerializer(serializers.Serializer): class PhotoSerializer(serializers.Serializer):
description = serializers.CharField() description = serializers.CharField()
album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), lookup_field='title', slug_url_kwarg='title') album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), lookup_field='title')
def restore_object(self, attrs, instance=None): def restore_object(self, attrs, instance=None):
return Photo(**attrs) return Photo(**attrs)
@ -94,7 +94,8 @@ class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
model_serializer_class = serializers.HyperlinkedModelSerializer model_serializer_class = serializers.HyperlinkedModelSerializer
urlpatterns = patterns('', urlpatterns = patterns(
'',
url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'), url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),
url(r'^basic/(?P<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'), url(r'^basic/(?P<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'),
url(r'^anchor/(?P<pk>\d+)/$', AnchorDetail.as_view(), name='anchor-detail'), url(r'^anchor/(?P<pk>\d+)/$', AnchorDetail.as_view(), name='anchor-detail'),
@ -110,7 +111,7 @@ urlpatterns = patterns('',
class TestBasicHyperlinkedView(TestCase): class TestBasicHyperlinkedView(TestCase):
urls = 'rest_framework.tests.test_hyperlinkedserializers' urls = 'tests.test_hyperlinkedserializers'
def setUp(self): def setUp(self):
""" """
@ -147,7 +148,7 @@ class TestBasicHyperlinkedView(TestCase):
class TestManyToManyHyperlinkedView(TestCase): class TestManyToManyHyperlinkedView(TestCase):
urls = 'rest_framework.tests.test_hyperlinkedserializers' urls = 'tests.test_hyperlinkedserializers'
def setUp(self): def setUp(self):
""" """
@ -195,7 +196,7 @@ class TestManyToManyHyperlinkedView(TestCase):
class TestHyperlinkedIdentityFieldLookup(TestCase): class TestHyperlinkedIdentityFieldLookup(TestCase):
urls = 'rest_framework.tests.test_hyperlinkedserializers' urls = 'tests.test_hyperlinkedserializers'
def setUp(self): def setUp(self):
""" """
@ -225,7 +226,7 @@ class TestHyperlinkedIdentityFieldLookup(TestCase):
class TestCreateWithForeignKeys(TestCase): class TestCreateWithForeignKeys(TestCase):
urls = 'rest_framework.tests.test_hyperlinkedserializers' urls = 'tests.test_hyperlinkedserializers'
def setUp(self): def setUp(self):
""" """
@ -250,7 +251,7 @@ class TestCreateWithForeignKeys(TestCase):
class TestCreateWithForeignKeysAndCustomSlug(TestCase): class TestCreateWithForeignKeysAndCustomSlug(TestCase):
urls = 'rest_framework.tests.test_hyperlinkedserializers' urls = 'tests.test_hyperlinkedserializers'
def setUp(self): def setUp(self):
""" """
@ -275,7 +276,7 @@ class TestCreateWithForeignKeysAndCustomSlug(TestCase):
class TestOptionalRelationHyperlinkedView(TestCase): class TestOptionalRelationHyperlinkedView(TestCase):
urls = 'rest_framework.tests.test_hyperlinkedserializers' urls = 'tests.test_hyperlinkedserializers'
def setUp(self): def setUp(self):
""" """
@ -335,7 +336,7 @@ class TestOverriddenURLField(TestCase):
class TestURLFieldNameBySettings(TestCase): class TestURLFieldNameBySettings(TestCase):
urls = 'rest_framework.tests.test_hyperlinkedserializers' urls = 'tests.test_hyperlinkedserializers'
def setUp(self): def setUp(self):
self.saved_url_field_name = api_settings.URL_FIELD_NAME self.saved_url_field_name = api_settings.URL_FIELD_NAME
@ -360,7 +361,7 @@ class TestURLFieldNameBySettings(TestCase):
class TestURLFieldNameByOptions(TestCase): class TestURLFieldNameByOptions(TestCase):
urls = 'rest_framework.tests.test_hyperlinkedserializers' urls = 'tests.test_hyperlinkedserializers'
def setUp(self): def setUp(self):
class Serializer(serializers.HyperlinkedModelSerializer): class Serializer(serializers.HyperlinkedModelSerializer):

View File

@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from rest_framework import serializers from rest_framework import serializers
from rest_framework.tests.models import RESTFrameworkModel from tests.models import RESTFrameworkModel
# Models # Models

View File

@ -1,10 +1,10 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from rest_framework.compat import patterns, url from django.conf.urls import patterns, url
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from rest_framework.tests.models import NullableForeignKeySource from tests.models import NullableForeignKeySource
from rest_framework.tests.serializers import NullableFKSourceSerializer from tests.serializers import NullableFKSourceSerializer
from rest_framework.tests.views import NullableFKSourceDetail from tests.views import NullableFKSourceDetail
urlpatterns = patterns( urlpatterns = patterns(
@ -18,7 +18,7 @@ class NullableForeignKeyTests(APITestCase):
DRF should be able to handle nullable foreign keys when a test DRF should be able to handle nullable foreign keys when a test
Client POST/PUT request is made with its own serialized object. Client POST/PUT request is made with its own serialized object.
""" """
urls = 'rest_framework.tests.test_nullable_fields' urls = 'tests.test_nullable_fields'
def test_updating_object_with_null_fk(self): def test_updating_object_with_null_fk(self):
obj = NullableForeignKeySource(name='example', target=None) obj = NullableForeignKeySource(name='example', target=None)

View File

@ -1,18 +1,17 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime import datetime
from decimal import Decimal from decimal import Decimal
from django.db import models
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import unittest
from rest_framework import generics, status, pagination, filters, serializers from rest_framework import generics, status, pagination, filters, serializers
from rest_framework.compat import django_filters from rest_framework.compat import django_filters
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import BasicModel from .models import BasicModel, FilterableItem
from .models import FilterableItem
factory = APIRequestFactory() factory = APIRequestFactory()
# Helper function to split arguments out of an url # Helper function to split arguments out of an url
def split_arguments_from_url(url): def split_arguments_from_url(url):
if '?' not in url: if '?' not in url:
@ -275,8 +274,8 @@ class TestUnpaginated(TestCase):
BasicModel(text=i).save() BasicModel(text=i).save()
self.objects = BasicModel.objects self.objects = BasicModel.objects
self.data = [ self.data = [
{'id': obj.id, 'text': obj.text} {'id': obj.id, 'text': obj.text}
for obj in self.objects.all() for obj in self.objects.all()
] ]
self.view = DefaultPageSizeKwargView.as_view() self.view = DefaultPageSizeKwargView.as_view()
@ -303,8 +302,8 @@ class TestCustomPaginateByParam(TestCase):
BasicModel(text=i).save() BasicModel(text=i).save()
self.objects = BasicModel.objects self.objects = BasicModel.objects
self.data = [ self.data = [
{'id': obj.id, 'text': obj.text} {'id': obj.id, 'text': obj.text}
for obj in self.objects.all() for obj in self.objects.all()
] ]
self.view = PaginateByParamView.as_view() self.view = PaginateByParamView.as_view()
@ -364,11 +363,11 @@ class TestMaxPaginateByParam(TestCase):
self.assertEqual(response.data['results'], self.data[:3]) self.assertEqual(response.data['results'], self.data[:3])
### Tests for context in pagination serializers # Tests for context in pagination serializers
class CustomField(serializers.Field): class CustomField(serializers.Field):
def to_native(self, value): def to_native(self, value):
if not 'view' in self.context: if 'view' not in self.context:
raise RuntimeError("context isn't getting passed into custom field") raise RuntimeError("context isn't getting passed into custom field")
return "value" return "value"
@ -378,7 +377,7 @@ class BasicModelSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(BasicModelSerializer, self).__init__(*args, **kwargs) super(BasicModelSerializer, self).__init__(*args, **kwargs)
if not 'view' in self.context: if 'view' not in self.context:
raise RuntimeError("context isn't getting passed into serializer init") raise RuntimeError("context isn't getting passed into serializer init")
@ -399,7 +398,7 @@ class TestContextPassedToCustomField(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
### Tests for custom pagination serializers # Tests for custom pagination serializers
class LinksSerializer(serializers.Serializer): class LinksSerializer(serializers.Serializer):
next = pagination.NextPageField(source='*') next = pagination.NextPageField(source='*')
@ -484,8 +483,6 @@ class NonIntegerPaginator(object):
class TestNonIntegerPagination(TestCase): class TestNonIntegerPagination(TestCase):
def test_custom_pagination_serializer(self): def test_custom_pagination_serializer(self):
objects = ['john', 'paul', 'george', 'ringo'] objects = ['john', 'paul', 'george', 'ringo']
paginator = NonIntegerPaginator(objects, 2) paginator = NonIntegerPaginator(objects, 2)

View File

@ -7,11 +7,12 @@ from rest_framework import generics, status, permissions, authentication, HTTP_H
from rest_framework.compat import guardian, get_model_name from rest_framework.compat import guardian, get_model_name
from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.filters import DjangoObjectPermissionsFilter
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import BasicModel from tests.models import BasicModel
import base64 import base64
factory = APIRequestFactory() factory = APIRequestFactory()
class RootView(generics.ListCreateAPIView): class RootView(generics.ListCreateAPIView):
model = BasicModel model = BasicModel
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
@ -101,42 +102,54 @@ class ModelPermissionsIntegrationTests(TestCase):
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_options_permitted(self): def test_options_permitted(self):
request = factory.options('/', request = factory.options(
HTTP_AUTHORIZATION=self.permitted_credentials) '/',
HTTP_AUTHORIZATION=self.permitted_credentials
)
response = root_view(request, pk='1') response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data) self.assertIn('actions', response.data)
self.assertEqual(list(response.data['actions'].keys()), ['POST']) self.assertEqual(list(response.data['actions'].keys()), ['POST'])
request = factory.options('/1', request = factory.options(
HTTP_AUTHORIZATION=self.permitted_credentials) '/1',
HTTP_AUTHORIZATION=self.permitted_credentials
)
response = instance_view(request, pk='1') response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data) self.assertIn('actions', response.data)
self.assertEqual(list(response.data['actions'].keys()), ['PUT']) self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
def test_options_disallowed(self): def test_options_disallowed(self):
request = factory.options('/', request = factory.options(
HTTP_AUTHORIZATION=self.disallowed_credentials) '/',
HTTP_AUTHORIZATION=self.disallowed_credentials
)
response = root_view(request, pk='1') response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data) self.assertNotIn('actions', response.data)
request = factory.options('/1', request = factory.options(
HTTP_AUTHORIZATION=self.disallowed_credentials) '/1',
HTTP_AUTHORIZATION=self.disallowed_credentials
)
response = instance_view(request, pk='1') response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data) self.assertNotIn('actions', response.data)
def test_options_updateonly(self): def test_options_updateonly(self):
request = factory.options('/', request = factory.options(
HTTP_AUTHORIZATION=self.updateonly_credentials) '/',
HTTP_AUTHORIZATION=self.updateonly_credentials
)
response = root_view(request, pk='1') response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data) self.assertNotIn('actions', response.data)
request = factory.options('/1', request = factory.options(
HTTP_AUTHORIZATION=self.updateonly_credentials) '/1',
HTTP_AUTHORIZATION=self.updateonly_credentials
)
response = instance_view(request, pk='1') response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data) self.assertIn('actions', response.data)
@ -153,6 +166,7 @@ class BasicPermModel(models.Model):
# add, change, delete built in to django # add, change, delete built in to django
) )
# Custom object-level permission, that includes 'view' permissions # Custom object-level permission, that includes 'view' permissions
class ViewObjectPermissions(permissions.DjangoObjectPermissions): class ViewObjectPermissions(permissions.DjangoObjectPermissions):
perms_map = { perms_map = {
@ -187,8 +201,7 @@ class ObjectPermissionsIntegrationTests(TestCase):
""" """
Integration tests for the object level permissions API. Integration tests for the object level permissions API.
""" """
@classmethod def setUp(self):
def setUpClass(cls):
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
# create users # create users
@ -206,7 +219,7 @@ class ObjectPermissionsIntegrationTests(TestCase):
app_label = BasicPermModel._meta.app_label app_label = BasicPermModel._meta.app_label
f = '{0}_{1}'.format f = '{0}_{1}'.format
perms = { perms = {
'view': f('view', model_name), 'view': f('view', model_name),
'change': f('change', model_name), 'change': f('change', model_name),
'delete': f('delete', model_name) 'delete': f('delete', model_name)
} }
@ -215,21 +228,13 @@ class ObjectPermissionsIntegrationTests(TestCase):
assign_perm(perm, everyone) assign_perm(perm, everyone)
everyone.user_set.add(*users.values()) everyone.user_set.add(*users.values())
cls.perms = perms
cls.users = users
def setUp(self):
from guardian.shortcuts import assign_perm
perms = self.perms
users = self.users
# appropriate object level permissions # appropriate object level permissions
readers = Group.objects.create(name='readers') readers = Group.objects.create(name='readers')
writers = Group.objects.create(name='writers') writers = Group.objects.create(name='writers')
deleters = Group.objects.create(name='deleters') deleters = Group.objects.create(name='deleters')
model = BasicPermModel.objects.create(text='foo') model = BasicPermModel.objects.create(text='foo')
assign_perm(perms['view'], readers, model) assign_perm(perms['view'], readers, model)
assign_perm(perms['change'], writers, model) assign_perm(perms['change'], writers, model)
assign_perm(perms['delete'], deleters, model) assign_perm(perms['delete'], deleters, model)
@ -255,21 +260,27 @@ class ObjectPermissionsIntegrationTests(TestCase):
# Update # Update
def test_can_update_permissions(self): def test_can_update_permissions(self):
request = factory.patch('/1', {'text': 'foobar'}, format='json', request = factory.patch(
HTTP_AUTHORIZATION=self.credentials['writeonly']) '/1', {'text': 'foobar'}, format='json',
HTTP_AUTHORIZATION=self.credentials['writeonly']
)
response = object_permissions_view(request, pk='1') response = object_permissions_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data.get('text'), 'foobar') self.assertEqual(response.data.get('text'), 'foobar')
def test_cannot_update_permissions(self): def test_cannot_update_permissions(self):
request = factory.patch('/1', {'text': 'foobar'}, format='json', request = factory.patch(
HTTP_AUTHORIZATION=self.credentials['deleteonly']) '/1', {'text': 'foobar'}, format='json',
HTTP_AUTHORIZATION=self.credentials['deleteonly']
)
response = object_permissions_view(request, pk='1') response = object_permissions_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_cannot_update_permissions_non_existing(self): def test_cannot_update_permissions_non_existing(self):
request = factory.patch('/999', {'text': 'foobar'}, format='json', request = factory.patch(
HTTP_AUTHORIZATION=self.credentials['deleteonly']) '/999', {'text': 'foobar'}, format='json',
HTTP_AUTHORIZATION=self.credentials['deleteonly']
)
response = object_permissions_view(request, pk='999') response = object_permissions_view(request, pk='999')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

View File

@ -7,7 +7,7 @@ from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import unittest
from rest_framework import serializers from rest_framework import serializers
from rest_framework.tests.models import BlogPost from tests.models import BlogPost
class NullModel(models.Model): class NullModel(models.Model):
@ -107,20 +107,26 @@ class RelatedFieldSourceTests(TestCase):
Check that the exception message are correct if the source field Check that the exception message are correct if the source field
doesn't exist. doesn't exist.
""" """
from rest_framework.tests.models import ManyToManySource from tests.models import ManyToManySource
class Meta: class Meta:
model = ManyToManySource model = ManyToManySource
attrs = { attrs = {
'name': serializers.SlugRelatedField( 'name': serializers.SlugRelatedField(
slug_field='name', source='banzai'), slug_field='name', source='banzai'),
'Meta': Meta, 'Meta': Meta,
} }
TestSerializer = type(str('TestSerializer'), TestSerializer = type(
(serializers.ModelSerializer,), attrs) str('TestSerializer'),
(serializers.ModelSerializer,),
attrs
)
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
TestSerializer(data={'name': 'foo'}) TestSerializer(data={'name': 'foo'})
@unittest.skipIf(get_version() < '1.6.0', 'Upstream behaviour changed in v1.6') @unittest.skipIf(get_version() < '1.6.0', 'Upstream behaviour changed in v1.6')
class RelatedFieldChoicesTests(TestCase): class RelatedFieldChoicesTests(TestCase):
""" """
@ -141,4 +147,3 @@ class RelatedFieldChoicesTests(TestCase):
widget_count = len(field.widget.choices) widget_count = len(field.widget.choices)
self.assertEqual(widget_count, choice_count + 1, 'BLANK_CHOICE_DASH option should have been added') self.assertEqual(widget_count, choice_count + 1, 'BLANK_CHOICE_DASH option should have been added')

View File

@ -1,9 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.urls import patterns, url
from django.test import TestCase from django.test import TestCase
from rest_framework import serializers from rest_framework import serializers
from rest_framework.compat import patterns, url
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import ( from tests.models import (
BlogPost, BlogPost,
ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
@ -16,7 +16,8 @@ request = factory.get('/') # Just to ensure we have a request in the serializer
def dummy_view(request, pk): def dummy_view(request, pk):
pass pass
urlpatterns = patterns('', urlpatterns = patterns(
'',
url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'), url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'),
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'), url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'), url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
@ -71,7 +72,7 @@ class NullableOneToOneTargetSerializer(serializers.HyperlinkedModelSerializer):
# TODO: Add test that .data cannot be accessed prior to .is_valid # TODO: Add test that .data cannot be accessed prior to .is_valid
class HyperlinkedManyToManyTests(TestCase): class HyperlinkedManyToManyTests(TestCase):
urls = 'rest_framework.tests.test_relations_hyperlink' urls = 'tests.test_relations_hyperlink'
def setUp(self): def setUp(self):
for idx in range(1, 4): for idx in range(1, 4):
@ -86,9 +87,9 @@ class HyperlinkedManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all() queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request}) serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
expected = [ expected = [
{'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']}, {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']},
{'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']}, {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
{'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']} {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
] ]
self.assertEqual(serializer.data, expected) self.assertEqual(serializer.data, expected)
@ -114,9 +115,9 @@ class HyperlinkedManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all() queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request}) serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
expected = [ expected = [
{'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}, {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']},
{'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']}, {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
{'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']} {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
] ]
self.assertEqual(serializer.data, expected) self.assertEqual(serializer.data, expected)
@ -179,7 +180,7 @@ class HyperlinkedManyToManyTests(TestCase):
class HyperlinkedForeignKeyTests(TestCase): class HyperlinkedForeignKeyTests(TestCase):
urls = 'rest_framework.tests.test_relations_hyperlink' urls = 'tests.test_relations_hyperlink'
def setUp(self): def setUp(self):
target = ForeignKeyTarget(name='target-1') target = ForeignKeyTarget(name='target-1')
@ -307,7 +308,7 @@ class HyperlinkedForeignKeyTests(TestCase):
class HyperlinkedNullableForeignKeyTests(TestCase): class HyperlinkedNullableForeignKeyTests(TestCase):
urls = 'rest_framework.tests.test_relations_hyperlink' urls = 'tests.test_relations_hyperlink'
def setUp(self): def setUp(self):
target = ForeignKeyTarget(name='target-1') target = ForeignKeyTarget(name='target-1')
@ -435,7 +436,7 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
class HyperlinkedNullableOneToOneTests(TestCase): class HyperlinkedNullableOneToOneTests(TestCase):
urls = 'rest_framework.tests.test_relations_hyperlink' urls = 'tests.test_relations_hyperlink'
def setUp(self): def setUp(self):
target = OneToOneTarget(name='target-1') target = OneToOneTarget(name='target-1')
@ -458,7 +459,7 @@ class HyperlinkedNullableOneToOneTests(TestCase):
# Regression tests for #694 (`source` attribute on related fields) # Regression tests for #694 (`source` attribute on related fields)
class HyperlinkedRelatedFieldSourceTests(TestCase): class HyperlinkedRelatedFieldSourceTests(TestCase):
urls = 'rest_framework.tests.test_relations_hyperlink' urls = 'tests.test_relations_hyperlink'
def test_related_manager_source(self): def test_related_manager_source(self):
""" """

View File

@ -1,12 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import six
from rest_framework import serializers from rest_framework import serializers
from rest_framework.tests.models import ( from tests.models import (
BlogPost, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, BlogPost, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource,
) )
from rest_framework.compat import six
# ManyToMany # ManyToMany
@ -65,9 +65,9 @@ class PKManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all() queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True) serializer = ManyToManySourceSerializer(queryset, many=True)
expected = [ expected = [
{'id': 1, 'name': 'source-1', 'targets': [1]}, {'id': 1, 'name': 'source-1', 'targets': [1]},
{'id': 2, 'name': 'source-2', 'targets': [1, 2]}, {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
{'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]} {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
] ]
self.assertEqual(serializer.data, expected) self.assertEqual(serializer.data, expected)
@ -93,9 +93,9 @@ class PKManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all() queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True) serializer = ManyToManySourceSerializer(queryset, many=True)
expected = [ expected = [
{'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]}, {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]},
{'id': 2, 'name': 'source-2', 'targets': [1, 2]}, {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
{'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]} {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
] ]
self.assertEqual(serializer.data, expected) self.assertEqual(serializer.data, expected)

View File

@ -1,6 +1,6 @@
from django.test import TestCase from django.test import TestCase
from rest_framework import serializers from rest_framework import serializers
from rest_framework.tests.models import NullableForeignKeySource, ForeignKeySource, ForeignKeyTarget from tests.models import NullableForeignKeySource, ForeignKeySource, ForeignKeyTarget
class ForeignKeyTargetSerializer(serializers.ModelSerializer): class ForeignKeyTargetSerializer(serializers.ModelSerializer):

View File

@ -2,13 +2,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from decimal import Decimal from decimal import Decimal
from django.conf.urls import patterns, url, include
from django.core.cache import cache from django.core.cache import cache
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import six, unittest
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import status, permissions from rest_framework import status, permissions
from rest_framework.compat import yaml, etree, patterns, url, include, six, StringIO from rest_framework.compat import yaml, etree, StringIO
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
@ -75,7 +76,6 @@ class MockGETView(APIView):
return Response({'foo': ['bar', 'baz']}) return Response({'foo': ['bar', 'baz']})
class MockPOSTView(APIView): class MockPOSTView(APIView):
def post(self, request, **kwargs): def post(self, request, **kwargs):
return Response({'foo': request.DATA}) return Response({'foo': request.DATA})
@ -101,7 +101,8 @@ class HTMLView1(APIView):
def get(self, request, **kwargs): def get(self, request, **kwargs):
return Response('text') return Response('text')
urlpatterns = patterns('', urlpatterns = patterns(
'',
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])), url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])), url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^cache$', MockGETView.as_view()), url(r'^cache$', MockGETView.as_view()),
@ -152,7 +153,7 @@ class RendererEndToEndTests(TestCase):
End-to-end testing of renderers using an RendererMixin on a generic view. End-to-end testing of renderers using an RendererMixin on a generic view.
""" """
urls = 'rest_framework.tests.test_renderers' urls = 'tests.test_renderers'
def test_default_renderer_serializes_content(self): def test_default_renderer_serializes_content(self):
"""If the Accept header is not set the default renderer should serialize the response.""" """If the Accept header is not set the default renderer should serialize the response."""
@ -311,16 +312,22 @@ class JSONRendererTests(TestCase):
class Dict(MutableMapping): class Dict(MutableMapping):
def __init__(self): def __init__(self):
self._dict = dict() self._dict = dict()
def __getitem__(self, key): def __getitem__(self, key):
return self._dict.__getitem__(key) return self._dict.__getitem__(key)
def __setitem__(self, key, value): def __setitem__(self, key, value):
return self._dict.__setitem__(key, value) return self._dict.__setitem__(key, value)
def __delitem__(self, key): def __delitem__(self, key):
return self._dict.__delitem__(key) return self._dict.__delitem__(key)
def __iter__(self): def __iter__(self):
return self._dict.__iter__() return self._dict.__iter__()
def __len__(self): def __len__(self):
return self._dict.__len__() return self._dict.__len__()
def keys(self): def keys(self):
return self._dict.keys() return self._dict.keys()
@ -329,22 +336,24 @@ class JSONRendererTests(TestCase):
x[2] = 3 x[2] = 3
ret = JSONRenderer().render(x) ret = JSONRenderer().render(x)
data = json.loads(ret.decode('utf-8')) data = json.loads(ret.decode('utf-8'))
self.assertEquals(data, {'key': 'string value', '2': 3}) self.assertEquals(data, {'key': 'string value', '2': 3})
def test_render_obj_with_getitem(self): def test_render_obj_with_getitem(self):
class DictLike(object): class DictLike(object):
def __init__(self): def __init__(self):
self._dict = {} self._dict = {}
def set(self, value): def set(self, value):
self._dict = dict(value) self._dict = dict(value)
def __getitem__(self, key): def __getitem__(self, key):
return self._dict[key] return self._dict[key]
x = DictLike() x = DictLike()
x.set({'a': 1, 'b': 'string'}) x.set({'a': 1, 'b': 'string'})
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
JSONRenderer().render(x) JSONRenderer().render(x)
def test_without_content_type_args(self): def test_without_content_type_args(self):
""" """
Test basic JSON rendering. Test basic JSON rendering.
@ -387,41 +396,53 @@ class JSONPRendererTests(TestCase):
Tests specific to the JSONP Renderer Tests specific to the JSONP Renderer
""" """
urls = 'rest_framework.tests.test_renderers' urls = 'tests.test_renderers'
def test_without_callback_with_json_renderer(self): def test_without_callback_with_json_renderer(self):
""" """
Test JSONP rendering with View JSON Renderer. Test JSONP rendering with View JSON Renderer.
""" """
resp = self.client.get('/jsonp/jsonrenderer', resp = self.client.get(
HTTP_ACCEPT='application/javascript') '/jsonp/jsonrenderer',
HTTP_ACCEPT='application/javascript'
)
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8') self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
self.assertEqual(resp.content, self.assertEqual(
('callback(%s);' % _flat_repr).encode('ascii')) resp.content,
('callback(%s);' % _flat_repr).encode('ascii')
)
def test_without_callback_without_json_renderer(self): def test_without_callback_without_json_renderer(self):
""" """
Test JSONP rendering without View JSON Renderer. Test JSONP rendering without View JSON Renderer.
""" """
resp = self.client.get('/jsonp/nojsonrenderer', resp = self.client.get(
HTTP_ACCEPT='application/javascript') '/jsonp/nojsonrenderer',
HTTP_ACCEPT='application/javascript'
)
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8') self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
self.assertEqual(resp.content, self.assertEqual(
('callback(%s);' % _flat_repr).encode('ascii')) resp.content,
('callback(%s);' % _flat_repr).encode('ascii')
)
def test_with_callback(self): def test_with_callback(self):
""" """
Test JSONP rendering with callback function name. Test JSONP rendering with callback function name.
""" """
callback_func = 'myjsonpcallback' callback_func = 'myjsonpcallback'
resp = self.client.get('/jsonp/nojsonrenderer?callback=' + callback_func, resp = self.client.get(
HTTP_ACCEPT='application/javascript') '/jsonp/nojsonrenderer?callback=' + callback_func,
HTTP_ACCEPT='application/javascript'
)
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8') self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
self.assertEqual(resp.content, self.assertEqual(
('%s(%s);' % (callback_func, _flat_repr)).encode('ascii')) resp.content,
('%s(%s);' % (callback_func, _flat_repr)).encode('ascii')
)
if yaml: if yaml:
@ -466,7 +487,6 @@ if yaml:
def assertYAMLContains(self, content, string): def assertYAMLContains(self, content, string):
self.assertTrue(string in content, '%r not in %r' % (string, content)) self.assertTrue(string in content, '%r not in %r' % (string, content))
class UnicodeYAMLRendererTests(TestCase): class UnicodeYAMLRendererTests(TestCase):
""" """
Tests specific for the Unicode YAML Renderer Tests specific for the Unicode YAML Renderer
@ -582,7 +602,7 @@ class CacheRenderTest(TestCase):
Tests specific to caching responses Tests specific to caching responses
""" """
urls = 'rest_framework.tests.test_renderers' urls = 'tests.test_renderers'
cache_key = 'just_a_cache_key' cache_key = 'just_a_cache_key'
@ -591,13 +611,13 @@ class CacheRenderTest(TestCase):
""" Return any errors that would be raised if `obj' is pickled """ Return any errors that would be raised if `obj' is pickled
Courtesy of koffie @ http://stackoverflow.com/a/7218986/109897 Courtesy of koffie @ http://stackoverflow.com/a/7218986/109897
""" """
if seen == None: if seen is None:
seen = [] seen = []
try: try:
state = obj.__getstate__() state = obj.__getstate__()
except AttributeError: except AttributeError:
return return
if state == None: if state is None:
return return
if isinstance(state, tuple): if isinstance(state, tuple):
if not isinstance(state[0], dict): if not isinstance(state[0], dict):

View File

@ -2,14 +2,15 @@
Tests for content parsing, and form-overloaded content parsing. Tests for content parsing, and form-overloaded content parsing.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.urls import patterns
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout from django.contrib.auth import authenticate, login, logout
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from django.core.handlers.wsgi import WSGIRequest from django.core.handlers.wsgi import WSGIRequest
from django.test import TestCase from django.test import TestCase
from django.utils import six
from rest_framework import status from rest_framework import status
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
from rest_framework.compat import patterns
from rest_framework.parsers import ( from rest_framework.parsers import (
BaseParser, BaseParser,
FormParser, FormParser,
@ -21,7 +22,6 @@ from rest_framework.response import Response
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory, APIClient from rest_framework.test import APIRequestFactory, APIClient
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.compat import six
from io import BytesIO from io import BytesIO
import json import json
@ -272,13 +272,14 @@ class MockView(APIView):
return Response(status=status.INTERNAL_SERVER_ERROR) return Response(status=status.INTERNAL_SERVER_ERROR)
urlpatterns = patterns('', urlpatterns = patterns(
'',
(r'^$', MockView.as_view()), (r'^$', MockView.as_view()),
) )
class TestContentParsingWithAuthentication(TestCase): class TestContentParsingWithAuthentication(TestCase):
urls = 'rest_framework.tests.test_request' urls = 'tests.test_request'
def setUp(self): def setUp(self):
self.csrf_client = APIClient(enforce_csrf_checks=True) self.csrf_client = APIClient(enforce_csrf_checks=True)

View File

@ -1,7 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.urls import patterns, url, include
from django.test import TestCase from django.test import TestCase
from rest_framework.tests.models import BasicModel, BasicModelSerializer from django.utils import six
from rest_framework.compat import patterns, url, include from tests.models import BasicModel, BasicModelSerializer
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework import generics from rest_framework import generics
@ -14,7 +15,6 @@ from rest_framework.renderers import (
) )
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.compat import six
class MockPickleRenderer(BaseRenderer): class MockPickleRenderer(BaseRenderer):
@ -100,7 +100,8 @@ new_model_viewset_router = routers.DefaultRouter()
new_model_viewset_router.register(r'', HTMLNewModelViewSet) new_model_viewset_router.register(r'', HTMLNewModelViewSet)
urlpatterns = patterns('', urlpatterns = patterns(
'',
url(r'^setbyview$', MockViewSettingContentType.as_view(renderer_classes=[RendererA, RendererB, RendererC])), url(r'^setbyview$', MockViewSettingContentType.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])), url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])), url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
@ -118,7 +119,7 @@ class RendererIntegrationTests(TestCase):
End-to-end testing of renderers using an ResponseMixin on a generic view. End-to-end testing of renderers using an ResponseMixin on a generic view.
""" """
urls = 'rest_framework.tests.test_response' urls = 'tests.test_response'
def test_default_renderer_serializes_content(self): def test_default_renderer_serializes_content(self):
"""If the Accept header is not set the default renderer should serialize the response.""" """If the Accept header is not set the default renderer should serialize the response."""
@ -198,7 +199,7 @@ class Issue122Tests(TestCase):
""" """
Tests that covers #122. Tests that covers #122.
""" """
urls = 'rest_framework.tests.test_response' urls = 'tests.test_response'
def test_only_html_renderer(self): def test_only_html_renderer(self):
""" """
@ -218,7 +219,7 @@ class Issue467Tests(TestCase):
Tests for #467 Tests for #467
""" """
urls = 'rest_framework.tests.test_response' urls = 'tests.test_response'
def test_form_has_label_and_help_text(self): def test_form_has_label_and_help_text(self):
resp = self.client.get('/html_new_model') resp = self.client.get('/html_new_model')
@ -232,7 +233,7 @@ class Issue807Tests(TestCase):
Covers #807 Covers #807
""" """
urls = 'rest_framework.tests.test_response' urls = 'tests.test_response'
def test_does_not_append_charset_by_default(self): def test_does_not_append_charset_by_default(self):
""" """

View File

@ -1,6 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.urls import patterns, url
from django.test import TestCase from django.test import TestCase
from rest_framework.compat import patterns, url
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
@ -10,7 +10,8 @@ factory = APIRequestFactory()
def null_view(request): def null_view(request):
pass pass
urlpatterns = patterns('', urlpatterns = patterns(
'',
url(r'^view$', null_view, name='view'), url(r'^view$', null_view, name='view'),
) )
@ -19,7 +20,7 @@ class ReverseTests(TestCase):
""" """
Tests for fully qualified URLs when using `reverse`. Tests for fully qualified URLs when using `reverse`.
""" """
urls = 'rest_framework.tests.test_reverse' urls = 'tests.test_reverse'
def test_reversed_urls_are_fully_qualified(self): def test_reversed_urls_are_fully_qualified(self):
request = factory.get('/view') request = factory.get('/view')

View File

@ -1,10 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.urls import patterns, url, include
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from rest_framework import serializers, viewsets, permissions from rest_framework import serializers, viewsets, permissions
from rest_framework.compat import include, patterns, url from rest_framework.decorators import detail_route, list_route
from rest_framework.decorators import link, action
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.routers import SimpleRouter, DefaultRouter from rest_framework.routers import SimpleRouter, DefaultRouter
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
@ -18,23 +18,23 @@ class BasicViewSet(viewsets.ViewSet):
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
return Response({'method': 'list'}) return Response({'method': 'list'})
@action() @detail_route(methods=['post'])
def action1(self, request, *args, **kwargs): def action1(self, request, *args, **kwargs):
return Response({'method': 'action1'}) return Response({'method': 'action1'})
@action() @detail_route(methods=['post'])
def action2(self, request, *args, **kwargs): def action2(self, request, *args, **kwargs):
return Response({'method': 'action2'}) return Response({'method': 'action2'})
@action(methods=['post', 'delete']) @detail_route(methods=['post', 'delete'])
def action3(self, request, *args, **kwargs): def action3(self, request, *args, **kwargs):
return Response({'method': 'action2'}) return Response({'method': 'action2'})
@link() @detail_route()
def link1(self, request, *args, **kwargs): def link1(self, request, *args, **kwargs):
return Response({'method': 'link1'}) return Response({'method': 'link1'})
@link() @detail_route()
def link2(self, request, *args, **kwargs): def link2(self, request, *args, **kwargs):
return Response({'method': 'link2'}) return Response({'method': 'link2'})
@ -72,7 +72,7 @@ class TestCustomLookupFields(TestCase):
""" """
Ensure that custom lookup fields are correctly routed. Ensure that custom lookup fields are correctly routed.
""" """
urls = 'rest_framework.tests.test_routers' urls = 'tests.test_routers'
def setUp(self): def setUp(self):
class NoteSerializer(serializers.HyperlinkedModelSerializer): class NoteSerializer(serializers.HyperlinkedModelSerializer):
@ -91,9 +91,10 @@ class TestCustomLookupFields(TestCase):
self.router = SimpleRouter() self.router = SimpleRouter()
self.router.register(r'notes', NoteViewSet) self.router.register(r'notes', NoteViewSet)
from rest_framework.tests import test_routers from tests import test_routers
urls = getattr(test_routers, 'urlpatterns') urls = getattr(test_routers, 'urlpatterns')
urls += patterns('', urls += patterns(
'',
url(r'^', include(self.router.urls)), url(r'^', include(self.router.urls)),
) )
@ -104,7 +105,8 @@ class TestCustomLookupFields(TestCase):
def test_retrieve_lookup_field_list_view(self): def test_retrieve_lookup_field_list_view(self):
response = self.client.get('/notes/') response = self.client.get('/notes/')
self.assertEqual(response.data, self.assertEqual(
response.data,
[{ [{
"url": "http://testserver/notes/123/", "url": "http://testserver/notes/123/",
"uuid": "123", "text": "foo bar" "uuid": "123", "text": "foo bar"
@ -113,7 +115,8 @@ class TestCustomLookupFields(TestCase):
def test_retrieve_lookup_field_detail_view(self): def test_retrieve_lookup_field_detail_view(self):
response = self.client.get('/notes/123/') response = self.client.get('/notes/123/')
self.assertEqual(response.data, self.assertEqual(
response.data,
{ {
"url": "http://testserver/notes/123/", "url": "http://testserver/notes/123/",
"uuid": "123", "text": "foo bar" "uuid": "123", "text": "foo bar"
@ -121,6 +124,27 @@ class TestCustomLookupFields(TestCase):
) )
class TestLookupValueRegex(TestCase):
"""
Ensure the router honors lookup_value_regex when applied
to the viewset.
"""
def setUp(self):
class NoteViewSet(viewsets.ModelViewSet):
queryset = RouterTestModel.objects.all()
lookup_field = 'uuid'
lookup_value_regex = '[0-9a-f]{32}'
self.router = SimpleRouter()
self.router.register(r'notes', NoteViewSet)
self.urls = self.router.urls
def test_urls_limited_by_lookup_value_regex(self):
expected = ['^notes/$', '^notes/(?P<uuid>[0-9a-f]{32})/$']
for idx in range(len(expected)):
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
class TestTrailingSlashIncluded(TestCase): class TestTrailingSlashIncluded(TestCase):
def setUp(self): def setUp(self):
class NoteViewSet(viewsets.ModelViewSet): class NoteViewSet(viewsets.ModelViewSet):
@ -131,7 +155,7 @@ class TestTrailingSlashIncluded(TestCase):
self.urls = self.router.urls self.urls = self.router.urls
def test_urls_have_trailing_slash_by_default(self): def test_urls_have_trailing_slash_by_default(self):
expected = ['^notes/$', '^notes/(?P<pk>[^/]+)/$'] expected = ['^notes/$', '^notes/(?P<pk>[^/.]+)/$']
for idx in range(len(expected)): for idx in range(len(expected)):
self.assertEqual(expected[idx], self.urls[idx].regex.pattern) self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
@ -175,7 +199,7 @@ class TestActionKeywordArgs(TestCase):
class TestViewSet(viewsets.ModelViewSet): class TestViewSet(viewsets.ModelViewSet):
permission_classes = [] permission_classes = []
@action(permission_classes=[permissions.AllowAny]) @detail_route(methods=['post'], permission_classes=[permissions.AllowAny])
def custom(self, request, *args, **kwargs): def custom(self, request, *args, **kwargs):
return Response({ return Response({
'permission_classes': self.permission_classes 'permission_classes': self.permission_classes
@ -196,14 +220,14 @@ class TestActionKeywordArgs(TestCase):
class TestActionAppliedToExistingRoute(TestCase): class TestActionAppliedToExistingRoute(TestCase):
""" """
Ensure `@action` decorator raises an except when applied Ensure `@detail_route` decorator raises an except when applied
to an existing route to an existing route
""" """
def test_exception_raised_when_action_applied_to_existing_route(self): def test_exception_raised_when_action_applied_to_existing_route(self):
class TestViewSet(viewsets.ModelViewSet): class TestViewSet(viewsets.ModelViewSet):
@action() @detail_route(methods=['post'])
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
return Response({ return Response({
'hello': 'world' 'hello': 'world'
@ -214,3 +238,49 @@ class TestActionAppliedToExistingRoute(TestCase):
with self.assertRaises(ImproperlyConfigured): with self.assertRaises(ImproperlyConfigured):
self.router.urls self.router.urls
class DynamicListAndDetailViewSet(viewsets.ViewSet):
def list(self, request, *args, **kwargs):
return Response({'method': 'list'})
@list_route(methods=['post'])
def list_route_post(self, request, *args, **kwargs):
return Response({'method': 'action1'})
@detail_route(methods=['post'])
def detail_route_post(self, request, *args, **kwargs):
return Response({'method': 'action2'})
@list_route()
def list_route_get(self, request, *args, **kwargs):
return Response({'method': 'link1'})
@detail_route()
def detail_route_get(self, request, *args, **kwargs):
return Response({'method': 'link2'})
class TestDynamicListAndDetailRouter(TestCase):
def setUp(self):
self.router = SimpleRouter()
def test_list_and_detail_route_decorators(self):
routes = self.router.get_routes(DynamicListAndDetailViewSet)
decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))]
# Make sure all these endpoints exist and none have been clobbered
for i, endpoint in enumerate(['list_route_get', 'list_route_post', 'detail_route_get', 'detail_route_post']):
route = decorator_routes[i]
# check url listing
if endpoint.startswith('list_'):
self.assertEqual(route.url,
'^{{prefix}}/{0}{{trailing_slash}}$'.format(endpoint))
else:
self.assertEqual(route.url,
'^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
# check method to function mapping
if endpoint.endswith('_post'):
method_map = 'post'
else:
method_map = 'get'
self.assertEqual(route.mapping[method_map], endpoint)

Some files were not shown because too many files have changed in this diff Show More