diff options
| -rw-r--r-- | .travis.yml | 15 | ||||
| -rw-r--r-- | CONTRIBUTING.md | 2 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rwxr-xr-x | docs/api-guide/authentication.md | 17 | ||||
| -rw-r--r-- | docs/api-guide/fields.md | 3 | ||||
| -rw-r--r-- | docs/api-guide/routers.md | 117 | ||||
| -rw-r--r-- | docs/api-guide/settings.md | 6 | ||||
| -rw-r--r-- | docs/api-guide/throttling.md | 13 | ||||
| -rw-r--r-- | docs/api-guide/viewsets.md | 28 | ||||
| -rw-r--r-- | docs/img/labels-and-milestones.png | bin | 0 -> 84026 bytes | |||
| -rw-r--r-- | docs/index.md | 18 | ||||
| -rw-r--r-- | docs/topics/2.4-accouncement.md | 147 | ||||
| -rw-r--r-- | docs/topics/contributing.md | 38 | ||||
| -rw-r--r-- | docs/topics/release-notes.md | 35 | ||||
| -rw-r--r-- | docs/tutorial/6-viewsets-and-routers.md | 8 | ||||
| -rw-r--r-- | requirements-test.txt (renamed from optionals.txt) | 7 | ||||
| -rw-r--r-- | rest_framework/__init__.py | 8 | ||||
| -rw-r--r-- | rest_framework/authentication.py | 4 | ||||
| -rw-r--r-- | rest_framework/authtoken/migrations/0001_initial.py | 94 | ||||
| -rw-r--r-- | rest_framework/authtoken/models.py | 1 | ||||
| -rw-r--r-- | rest_framework/authtoken/south_migrations/0001_initial.py | 60 | ||||
| -rw-r--r-- | rest_framework/authtoken/south_migrations/__init__.py (renamed from rest_framework/runtests/__init__.py) | 0 | ||||
| -rw-r--r-- | rest_framework/compat.py | 374 | ||||
| -rw-r--r-- | rest_framework/decorators.py | 45 | ||||
| -rw-r--r-- | rest_framework/exceptions.py | 1 | ||||
| -rw-r--r-- | rest_framework/fields.py | 26 | ||||
| -rw-r--r-- | rest_framework/filters.py | 7 | ||||
| -rw-r--r-- | rest_framework/generics.py | 73 | ||||
| -rw-r--r-- | rest_framework/mixins.py | 8 | ||||
| -rw-r--r-- | rest_framework/negotiation.py | 6 | ||||
| -rw-r--r-- | rest_framework/parsers.py | 3 | ||||
| -rw-r--r-- | rest_framework/permissions.py | 36 | ||||
| -rw-r--r-- | rest_framework/relations.py | 121 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 25 | ||||
| -rw-r--r-- | rest_framework/request.py | 27 | ||||
| -rw-r--r-- | rest_framework/response.py | 8 | ||||
| -rw-r--r-- | rest_framework/routers.py | 71 | ||||
| -rwxr-xr-x | rest_framework/runtests/runcoverage.py | 78 | ||||
| -rwxr-xr-x | rest_framework/runtests/runtests.py | 52 | ||||
| -rw-r--r-- | rest_framework/runtests/urls.py | 7 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 67 | ||||
| -rw-r--r-- | rest_framework/settings.py | 19 | ||||
| -rw-r--r-- | rest_framework/six.py | 389 | ||||
| -rw-r--r-- | rest_framework/status.py | 4 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/base.html | 1 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/login_base.html | 1 | ||||
| -rw-r--r-- | rest_framework/templatetags/rest_framework.py | 95 | ||||
| -rw-r--r-- | rest_framework/test.py | 10 | ||||
| -rw-r--r-- | rest_framework/tests/test_breadcrumbs.py | 73 | ||||
| -rw-r--r-- | rest_framework/tests/tests.py | 16 | ||||
| -rw-r--r-- | rest_framework/throttling.py | 25 | ||||
| -rw-r--r-- | rest_framework/urlpatterns.py | 2 | ||||
| -rw-r--r-- | rest_framework/urls.py | 12 | ||||
| -rw-r--r-- | rest_framework/utils/encoders.py | 34 | ||||
| -rw-r--r-- | rest_framework/utils/formatting.py | 4 | ||||
| -rw-r--r-- | rest_framework/utils/mediatypes.py | 4 | ||||
| -rw-r--r-- | rest_framework/views.py | 2 | ||||
| -rw-r--r-- | rest_framework/viewsets.py | 10 | ||||
| -rwxr-xr-x | runtests.py | 86 | ||||
| -rwxr-xr-x | setup.py | 17 | ||||
| -rw-r--r-- | tests/__init__.py (renamed from rest_framework/tests/__init__.py) | 0 | ||||
| -rw-r--r-- | tests/accounts/__init__.py (renamed from rest_framework/tests/accounts/__init__.py) | 0 | ||||
| -rw-r--r-- | tests/accounts/models.py (renamed from rest_framework/tests/accounts/models.py) | 2 | ||||
| -rw-r--r-- | tests/accounts/serializers.py (renamed from rest_framework/tests/accounts/serializers.py) | 4 | ||||
| -rw-r--r-- | tests/conftest.py | 88 | ||||
| -rw-r--r-- | tests/description.py (renamed from rest_framework/tests/description.py) | 0 | ||||
| -rw-r--r-- | tests/extras/__init__.py (renamed from rest_framework/tests/extras/__init__.py) | 0 | ||||
| -rw-r--r-- | tests/extras/bad_import.py (renamed from rest_framework/tests/extras/bad_import.py) | 0 | ||||
| -rw-r--r-- | tests/models.py (renamed from rest_framework/tests/models.py) | 19 | ||||
| -rw-r--r-- | tests/records/__init__.py (renamed from rest_framework/tests/records/__init__.py) | 0 | ||||
| -rw-r--r-- | tests/records/models.py (renamed from rest_framework/tests/records/models.py) | 0 | ||||
| -rw-r--r-- | tests/serializers.py (renamed from rest_framework/tests/serializers.py) | 3 | ||||
| -rw-r--r-- | tests/settings.py (renamed from rest_framework/runtests/settings.py) | 26 | ||||
| -rw-r--r-- | tests/test_authentication.py (renamed from rest_framework/tests/test_authentication.py) | 87 | ||||
| -rw-r--r-- | tests/test_breadcrumbs.py | 100 | ||||
| -rw-r--r-- | tests/test_decorators.py (renamed from rest_framework/tests/test_decorators.py) | 0 | ||||
| -rw-r--r-- | tests/test_description.py (renamed from rest_framework/tests/test_description.py) | 4 | ||||
| -rw-r--r-- | tests/test_fields.py (renamed from rest_framework/tests/test_fields.py) | 16 | ||||
| -rw-r--r-- | tests/test_files.py (renamed from rest_framework/tests/test_files.py) | 13 | ||||
| -rw-r--r-- | tests/test_filters.py (renamed from rest_framework/tests/test_filters.py) | 42 | ||||
| -rw-r--r-- | tests/test_genericrelations.py (renamed from rest_framework/tests/test_genericrelations.py) | 26 | ||||
| -rw-r--r-- | tests/test_generics.py (renamed from rest_framework/tests/test_generics.py) | 6 | ||||
| -rw-r--r-- | tests/test_htmlrenderer.py (renamed from rest_framework/tests/test_htmlrenderer.py) | 13 | ||||
| -rw-r--r-- | tests/test_hyperlinkedserializers.py (renamed from rest_framework/tests/test_hyperlinkedserializers.py) | 25 | ||||
| -rw-r--r-- | tests/test_multitable_inheritance.py (renamed from rest_framework/tests/test_multitable_inheritance.py) | 2 | ||||
| -rw-r--r-- | tests/test_negotiation.py (renamed from rest_framework/tests/test_negotiation.py) | 0 | ||||
| -rw-r--r-- | tests/test_nullable_fields.py (renamed from rest_framework/tests/test_nullable_fields.py) | 10 | ||||
| -rw-r--r-- | tests/test_pagination.py (renamed from rest_framework/tests/test_pagination.py) | 23 | ||||
| -rw-r--r-- | tests/test_parsers.py (renamed from rest_framework/tests/test_parsers.py) | 0 | ||||
| -rw-r--r-- | tests/test_permissions.py (renamed from rest_framework/tests/test_permissions.py) | 73 | ||||
| -rw-r--r-- | tests/test_relations.py (renamed from rest_framework/tests/test_relations.py) | 15 | ||||
| -rw-r--r-- | tests/test_relations_hyperlink.py (renamed from rest_framework/tests/test_relations_hyperlink.py) | 29 | ||||
| -rw-r--r-- | tests/test_relations_nested.py (renamed from rest_framework/tests/test_relations_nested.py) | 0 | ||||
| -rw-r--r-- | tests/test_relations_pk.py (renamed from rest_framework/tests/test_relations_pk.py) | 16 | ||||
| -rw-r--r-- | tests/test_relations_slug.py (renamed from rest_framework/tests/test_relations_slug.py) | 2 | ||||
| -rw-r--r-- | tests/test_renderers.py (renamed from rest_framework/tests/test_renderers.py) | 70 | ||||
| -rw-r--r-- | tests/test_request.py (renamed from rest_framework/tests/test_request.py) | 9 | ||||
| -rw-r--r-- | tests/test_response.py (renamed from rest_framework/tests/test_response.py) | 17 | ||||
| -rw-r--r-- | tests/test_reverse.py (renamed from rest_framework/tests/test_reverse.py) | 7 | ||||
| -rw-r--r-- | tests/test_routers.py (renamed from rest_framework/tests/test_routers.py) | 102 | ||||
| -rw-r--r-- | tests/test_serializer.py (renamed from rest_framework/tests/test_serializer.py) | 74 | ||||
| -rw-r--r-- | tests/test_serializer_bulk_update.py (renamed from rest_framework/tests/test_serializer_bulk_update.py) | 6 | ||||
| -rw-r--r-- | tests/test_serializer_empty.py (renamed from rest_framework/tests/test_serializer_empty.py) | 0 | ||||
| -rw-r--r-- | tests/test_serializer_import.py (renamed from rest_framework/tests/test_serializer_import.py) | 2 | ||||
| -rw-r--r-- | tests/test_serializer_nested.py (renamed from rest_framework/tests/test_serializer_nested.py) | 2 | ||||
| -rw-r--r-- | tests/test_serializers.py (renamed from rest_framework/tests/test_serializers.py) | 6 | ||||
| -rw-r--r-- | tests/test_settings.py (renamed from rest_framework/tests/test_settings.py) | 4 | ||||
| -rw-r--r-- | tests/test_status.py (renamed from rest_framework/tests/test_status.py) | 2 | ||||
| -rw-r--r-- | tests/test_templatetags.py (renamed from rest_framework/tests/test_templatetags.py) | 4 | ||||
| -rw-r--r-- | tests/test_testing.py (renamed from rest_framework/tests/test_testing.py) | 10 | ||||
| -rw-r--r-- | tests/test_throttling.py (renamed from rest_framework/tests/test_throttling.py) | 114 | ||||
| -rw-r--r-- | tests/test_urlizer.py (renamed from rest_framework/tests/test_urlizer.py) | 1 | ||||
| -rw-r--r-- | tests/test_urlpatterns.py (renamed from rest_framework/tests/test_urlpatterns.py) | 2 | ||||
| -rw-r--r-- | tests/test_validation.py (renamed from rest_framework/tests/test_validation.py) | 0 | ||||
| -rw-r--r-- | tests/test_views.py (renamed from rest_framework/tests/test_views.py) | 0 | ||||
| -rw-r--r-- | tests/test_write_only_fields.py (renamed from rest_framework/tests/test_write_only_fields.py) | 0 | ||||
| -rw-r--r-- | tests/urls.py | 6 | ||||
| -rw-r--r-- | tests/users/__init__.py (renamed from rest_framework/tests/users/__init__.py) | 0 | ||||
| -rw-r--r-- | tests/users/models.py (renamed from rest_framework/tests/users/models.py) | 0 | ||||
| -rw-r--r-- | tests/users/serializers.py (renamed from rest_framework/tests/users/serializers.py) | 2 | ||||
| -rw-r--r-- | tests/utils.py (renamed from rest_framework/tests/utils.py) | 2 | ||||
| -rw-r--r-- | tests/views.py (renamed from rest_framework/tests/views.py) | 4 | ||||
| -rw-r--r-- | tox.ini | 50 | 
123 files changed, 1753 insertions, 1849 deletions
| diff --git a/.travis.yml b/.travis.yml index 7f1fda83..ececf3e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,11 +12,14 @@ env:    - DJANGO="django==1.6.5"    - DJANGO="django==1.5.8"    - DJANGO="django==1.4.13" -  - DJANGO="django==1.3.7"  install:    - 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 django-oauth-plus==2.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=.  script: -  - python rest_framework/runtests/runtests.py +  - ./runtests.py  matrix:    exclude: @@ -34,13 +37,7 @@ matrix:        env: DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/"      - python: "3.2"        env: DJANGO="django==1.4.13" -    - python: "3.2" -      env: DJANGO="django==1.3.7"      - python: "3.3"        env: DJANGO="django==1.4.13" -    - python: "3.3" -      env: DJANGO="django==1.3.7"      - python: "3.4"        env: DJANGO="django==1.4.13" -    - python: "3.4" -      env: DJANGO="django==1.3.7" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7aa6fc4..ff6018b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,7 +65,7 @@ To run the tests, clone the repository, and then:      pip install -r optionals.txt      # 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: @@ -27,7 +27,7 @@ There is a live example API for testing purposes, [available here][sandbox].  # Requirements  * Python (2.6.5+, 2.7, 3.2, 3.3) -* Django (1.3, 1.4, 1.5, 1.6) +* Django (1.4.2+, 1.5, 1.6, 1.7)  # Installation diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 1cb37d67..343466ee 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -126,7 +126,13 @@ To use the `TokenAuthentication` scheme you'll need to [configure the authentica          '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. @@ -198,7 +204,14 @@ Note that the default `obtain_auth_token` view explicitly uses JSON requests and  #### 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. diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index b41e0ebc..95d9fad3 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -164,11 +164,12 @@ Corresponds to `django.db.models.fields.BooleanField`.  ## CharField  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`  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 diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 64f05af3..2d760ca4 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -51,36 +51,41 @@ This means you'll need to explicitly set the `base_name` argument when registeri  ### 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: -	from myapp.permissions import IsAdminOrIsSelf -    from rest_framework.decorators import action - -    @action(permission_classes=[IsAdminOrIsSelf]) -    def set_password(self, request, pk=None): +    from myapp.permissions import IsAdminOrIsSelf +    from rest_framework.decorators import detail_route +     +    class UserViewSet(ModelViewSet):          ... +         +        @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf]) +        def set_password(self, request, pk=None): +            ...  The following URL pattern would additionally be generated:  * 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  ## 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>      <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>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>PUT</td><td>update</td></tr>      <tr><td>PATCH</td><td>partial_update</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>POST</td><td>@action decorated method</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>  </table>  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. +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  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 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>{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>PUT</td><td>update</td></tr>      <tr><td>PATCH</td><td>partial_update</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>POST</td><td>@action decorated method</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>  </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. @@ -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. +## 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  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.          """          routes = [ -            Route(url=r'^{prefix}$', -                  mapping={'get': 'list'}, -                  name='{basename}-list', -                  initkwargs={'suffix': 'List'}), -            Route(url=r'^{prefix}/{lookup}$', -                  mapping={'get': 'retrieve'}, -                  name='{basename}-detail', -                  initkwargs={'suffix': 'Detail'}) +            Route( +            	url=r'^{prefix}$', +            	mapping={'get': 'list'}, +            	name='{basename}-list', +            	initkwargs={'suffix': 'List'} +            ), +            Route( +            	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 @@ -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].  [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  [wq.db]: http://wq.io/wq.db  [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-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-customizable-endpoint-names]: http://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name
\ No newline at end of file +[drf-extensions-customizable-endpoint-names]: http://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index c979019f..8bde4d87 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -377,5 +377,11 @@ The name of a parameter in the URL conf that may be used to provide a format suf  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/  [strftime]: http://docs.python.org/2/library/time.html#time.strftime diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 92f4c22b..832304f1 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -35,7 +35,7 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C          'DEFAULT_THROTTLE_RATES': {              'anon': '100/day',              'user': '1000/day' -        }         +        }      }  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) +## 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  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  [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-docs]: https://docs.djangoproject.com/en/dev/topics/cache/#setting-up-the-cache diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index aa2ceb7f..9030e3ee 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -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. -## 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: @@ -101,14 +101,16 @@ The default routers included with REST framework will provide routes for a stand          def destroy(self, request, pk=None):              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:      from django.contrib.auth.models import User -    from rest_framework import viewsets      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 myapp.serializers import UserSerializer, PasswordSerializer @@ -119,7 +121,7 @@ For example:          queryset = User.objects.all()          serializer_class = UserSerializer -        @action() +        @detail_route(methods=['post'])          def set_password(self, request, pk=None):              user = self.get_object()              serializer = PasswordSerializer(data=request.DATA) @@ -131,21 +133,27 @@ For example:                  return Response(serializer.errors,                                  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) + +The decorators can additionally take extra arguments that will be set for the routed view only.  For example... -        @action(permission_classes=[IsAdminOrIsSelf]) +        @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf])          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):             ...  The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$` -  ---  # API Reference diff --git a/docs/img/labels-and-milestones.png b/docs/img/labels-and-milestones.pngBinary files differ new file mode 100644 index 00000000..e7c829ad --- /dev/null +++ b/docs/img/labels-and-milestones.png diff --git a/docs/index.md b/docs/index.md index dd407497..83e30a69 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,7 +50,7 @@ Some reasons you might want to use REST framework:  REST framework requires the following:  * Python (2.6.5+, 2.7, 3.2, 3.3) -* Django (1.3, 1.4, 1.5, 1.6) +* Django (1.4.2+, 1.5, 1.6, 1.7)  The following packages are optional: @@ -207,19 +207,9 @@ General guides to using REST framework.  ## Development -If you want to work on REST framework itself, clone the repository, then... - -Build the docs: - -    ./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 +See the [Contribution guidelines][contributing] for information on how to clone +the repository, run the test suite and contribute changes back to REST +Framework.  ## Support diff --git a/docs/topics/2.4-accouncement.md b/docs/topics/2.4-accouncement.md new file mode 100644 index 00000000..50484287 --- /dev/null +++ b/docs/topics/2.4-accouncement.md @@ -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. + +--- + + + +**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 diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 18a05050..3400bc8f 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -62,10 +62,44 @@ To run the tests, clone the repository, and then:      virtualenv env      source env/bin/activate      pip install -r requirements.txt -    pip install -r optionals.txt +    pip install -r requirements-test.txt      # 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: diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index ea4c912c..a31be28f 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -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.14 @@ -169,9 +196,9 @@ You can determine your currently installed version using `pip freeze`:  * Added `trailing_slash` option to routers.  * Include support for `HttpStreamingResponse`.  * 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. -* 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: Fix `ScopedRateThrottle`. @@ -212,7 +239,7 @@ You can determine your currently installed version using `pip freeze`:  * Added SearchFilter  * Added OrderingFilter  * 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  ### 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.  * Bugfix: Fix regression with DjangoFilterBackend not worthing correctly with single object views.  * 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 diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 04b42f2e..b2019520 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -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. -    from rest_framework.decorators import link +    from rest_framework.decorators import detail_route      class SnippetViewSet(viewsets.ModelViewSet):          """ @@ -39,7 +39,7 @@ Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighl          permission_classes = (permissions.IsAuthenticatedOrReadOnly,                                IsOwnerOrReadOnly,) -        @link(renderer_classes=[renderers.StaticHTMLRenderer]) +        @detail_route(renderer_classes=[renderers.StaticHTMLRenderer])          def highlight(self, request, *args, **kwargs):              snippet = self.get_object()              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. -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 diff --git a/optionals.txt b/requirements-test.txt index 262e7644..411daeba 100644 --- a/optionals.txt +++ b/requirements-test.txt @@ -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  PyYAML>=3.10  defusedxml>=0.3 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 01036cef..f30012b9 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,9 +1,9 @@  """ -______ _____ _____ _____    __                                             _     -| ___ \  ___/  ___|_   _|  / _|                                           | |    -| |_/ / |__ \ `--.  | |   | |_ _ __ __ _ _ __ ___   _____      _____  _ __| | __ +______ _____ _____ _____    __ +| ___ \  ___/  ___|_   _|  / _|                                           | | +| |_/ / |__ \ `--.  | |   | |_ _ __ __ _ _ __ ___   _____      _____  _ __| |__  |    /|  __| `--. \ | |   |  _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ / -| |\ \| |___/\__/ / | |   | | | | | (_| | | | | | |  __/\ V  V / (_) | |  |   <  +| |\ \| |___/\__/ / | |   | | | | | (_| | | | | | |  __/\ V  V / (_) | |  |   <  \_| \_\____/\____/  \_/   |_| |_|  \__,_|_| |_| |_|\___| \_/\_/ \___/|_|  |_|\_|  """ diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 887ef5d7..5721a869 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -6,9 +6,9 @@ import base64  from django.contrib.auth import authenticate  from django.core.exceptions import ImproperlyConfigured +from django.middleware.csrf import CsrfViewMiddleware  from django.conf import settings  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 oauth2_provider, provider_now, check_nonce  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.      """      auth = request.META.get('HTTP_AUTHORIZATION', b'') -    if type(auth) == type(''): +    if isinstance(auth, type('')):          # Work around django test client oddness          auth = auth.encode(HTTP_HEADER_ENCODING)      return auth diff --git a/rest_framework/authtoken/migrations/0001_initial.py b/rest_framework/authtoken/migrations/0001_initial.py index d5965e40..2e5d6b47 100644 --- a/rest_framework/authtoken/migrations/0001_initial.py +++ b/rest_framework/authtoken/migrations/0001_initial.py @@ -1,67 +1,27 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - -from rest_framework.settings import api_settings - - -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'] +# encoding: utf8 +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + +    dependencies = [ +        migrations.swappable_dependency(settings.AUTH_USER_MODEL), +    ] + +    operations = [ +        migrations.CreateModel( +            name='Token', +            fields=[ +                ('key', models.CharField(max_length=40, serialize=False, primary_key=True)), +                ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, to_field='id')), +                ('created', models.DateTimeField(auto_now_add=True)), +            ], +            options={ +                'abstract': False, +            }, +            bases=(models.Model,), +        ), +    ] diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 167fa531..db21d44c 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -1,6 +1,5 @@  import binascii  import os -from hashlib import sha1  from django.conf import settings  from django.db import models diff --git a/rest_framework/authtoken/south_migrations/0001_initial.py b/rest_framework/authtoken/south_migrations/0001_initial.py new file mode 100644 index 00000000..926de02b --- /dev/null +++ b/rest_framework/authtoken/south_migrations/0001_initial.py @@ -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'] diff --git a/rest_framework/runtests/__init__.py b/rest_framework/authtoken/south_migrations/__init__.py index e69de29b..e69de29b 100644 --- a/rest_framework/runtests/__init__.py +++ b/rest_framework/authtoken/south_migrations/__init__.py diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 9ad8b0d2..fa0f0bfb 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -5,25 +5,14 @@ versions of django/python, and compatibility wrappers around optional packages.  # flake8: noqa  from __future__ import unicode_literals -  import django  import inspect  from django.core.exceptions import ImproperlyConfigured  from django.conf import settings +from django.utils import six -# 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 -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: +# Handle django.utils.encoding rename in 1.5 onwards.  # smart_unicode -> smart_text  # force_unicode -> force_text  try: @@ -42,12 +31,14 @@ try:  except ImportError:      from django.http import HttpResponse as HttpResponseBase +  # django-filter is optional  try:      import django_filters  except ImportError:      django_filters = None +  # Django-guardian is optional. Import only if guardian is in INSTALLED_APPS  # Fixes (#1712). We keep the try/except for the test suite.  guardian = None @@ -108,46 +99,13 @@ def get_concrete_model(model_cls):          return model_cls +# View._allowed_methods only present from 1.5 onwards  if django.VERSION >= (1, 5):      from django.views.generic import View  else: -    from django.views.generic import View as _View -    from django.utils.decorators import classonlymethod -    from django.utils.functional import update_wrapper - -    class View(_View): -        # 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 +    from django.views.generic import View as DjangoView + +    class View(DjangoView):          def _allowed_methods(self):              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'] -# PUT, DELETE do not require CSRF until 1.4.  They should.  Make it better. -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 - +# RequestFactory only provides `generic` from 1.5 onwards  from django.test.client import RequestFactory as DjangoRequestFactory  from django.test.client import FakePayload  try:      # In 1.5 the test client uses force_bytes      from django.utils.encoding import force_bytes as force_bytes_or_smart_bytes  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 -  class RequestFactory(DjangoRequestFactory):      def generic(self, method, path,              data='', content_type='application/octet-stream', **extra): @@ -491,6 +149,7 @@ class RequestFactory(DjangoRequestFactory):          r.update(extra)          return self.request(**r) +  # Markdown is optional  try:      import markdown @@ -505,7 +164,6 @@ try:          safe_mode = False          md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode)          return md.convert(text) -  except ImportError:      apply_markdown = None @@ -523,14 +181,16 @@ try:  except ImportError:      etree = None -# OAuth is optional + +# OAuth2 is optional  try:      # Note: The `oauth2` package actually provides oauth1.0a support.  Urg.      import oauth2 as oauth  except ImportError:      oauth = None -# OAuth is optional + +# OAuthProvider is optional  try:      import oauth_provider      from oauth_provider.store import store as oauth_provider_store @@ -552,6 +212,7 @@ except (ImportError, ImproperlyConfigured):      oauth_provider_store = None      check_nonce = None +  # OAuth 2 support is optional  try:      import provider as oauth2_provider @@ -571,7 +232,8 @@ except ImportError:      oauth2_constants = None      provider_now = None -# Handle lazy strings + +# Handle lazy strings across Py2/Py3  from django.utils.functional import Promise  if six.PY3: diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index c69756a4..449ba0a2 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -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.  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.  """  from __future__ import unicode_literals -from rest_framework.compat import six +from django.utils import six  from rest_framework.views import APIView  import types +import warnings  def api_view(http_method_names): @@ -107,23 +108,59 @@ def permission_classes(permission_classes):      return decorator +def detail_route(methods=['get'], **kwargs): +    """ +    Used to mark a method on a ViewSet that should be routed for detail requests. +    """ +    def decorator(func): +        func.bind_to_methods = methods +        func.detail = True +        func.kwargs = kwargs +        return func +    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 GET requests. +    Used to mark a method on a ViewSet that should be routed for detail GET requests.      """ +    msg = 'link is pending deprecation. Use detail_route instead.' +    warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) +      def decorator(func):          func.bind_to_methods = ['get'] +        func.detail = True          func.kwargs = kwargs          return func +      return decorator  def action(methods=['post'], **kwargs):      """ -    Used to mark a method on a ViewSet that should be routed for 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):          func.bind_to_methods = methods +        func.detail = True          func.kwargs = kwargs          return func +      return decorator diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 5f774a9f..97dab77e 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -23,6 +23,7 @@ class APIException(Exception):      def __str__(self):          return self.detail +  class ParseError(APIException):      status_code = status.HTTP_400_BAD_REQUEST      default_detail = 'Malformed request.' diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6caae924..9d707c9b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -18,12 +18,14 @@ from django.conf import settings  from django.db.models.fields import BLANK_CHOICE_DASH  from django.http import QueryDict  from django.forms import widgets +from django.utils import six, timezone  from django.utils.encoding import is_protected_type  from django.utils.translation import ugettext_lazy as _  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.compat import ( -    timezone, parse_date, parse_datetime, parse_time, BytesIO, six, smart_text, +    BytesIO, smart_text,      force_text, is_non_str_iterable  )  from rest_framework.settings import api_settings @@ -61,8 +63,10 @@ def get_component(obj, attr_name):  def readable_datetime_formats(formats): -    format = ', '.join(formats).replace(ISO_8601, -             'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]') +    format = ', '.join(formats).replace( +        ISO_8601, +        'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]' +    )      return humanize_strptime(format) @@ -265,13 +269,6 @@ class WritableField(Field):                   validators=[], error_messages=None, widget=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)          self.read_only = read_only @@ -430,7 +427,7 @@ class ModelField(WritableField):          } -##### Typed Fields ##### +# Typed Fields  class BooleanField(WritableField):      type_name = 'BooleanField' @@ -465,8 +462,9 @@ class CharField(WritableField):      type_label = 'string'      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.allow_none = allow_none          super(CharField, self).__init__(*args, **kwargs)          if min_length is not None:              self.validators.append(validators.MinLengthValidator(min_length)) @@ -477,7 +475,7 @@ class CharField(WritableField):          if isinstance(value, six.string_types):              return value -        if value is None: +        if value is None and not self.allow_none:              return ''          return smart_text(value) @@ -488,7 +486,7 @@ class URLField(CharField):      type_label = 'url'      def __init__(self, **kwargs): -        if not 'validators' in kwargs: +        if 'validators' not in kwargs:              kwargs['validators'] = [validators.URLValidator()]          super(URLField, self).__init__(**kwargs) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index c3b846ae..e2080013 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -5,7 +5,8 @@ returned by list views.  from __future__ import unicode_literals  from django.core.exceptions import ImproperlyConfigured  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 functools import reduce  import operator @@ -44,7 +45,7 @@ class DjangoFilterBackend(BaseFilterBackend):          if filter_class:              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' % \                  (filter_model, queryset.model) @@ -116,7 +117,7 @@ class OrderingFilter(BaseFilterBackend):      def get_ordering(self, request):          """          Ordering is set by a comma delimited ?ordering=... query parameter. -         +          The `ordering` query parameter can be overridden by setting          the `ordering_param` value on the OrderingFilter or by          specifying an `ORDERING_PARAM` value in the API settings. diff --git a/rest_framework/generics.py b/rest_framework/generics.py index aea636f1..77deb8e4 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -25,6 +25,7 @@ def strict_positive_int(integer_string, cutoff=None):          ret = min(ret, cutoff)      return ret +  def get_object_or_404(queryset, *filter_args, **filter_kwargs):      """      Same as Django's standard shortcut, but make sure to raise 404 @@ -127,11 +128,11 @@ class GenericAPIView(views.APIView):          deprecated_style = False          if page_size is not None:              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 '                            'changed, and will simply return a page object '                            'when called without a `page_size` argument.', -                          PendingDeprecationWarning, stacklevel=2) +                          DeprecationWarning, stacklevel=2)              deprecated_style = True          else:              # Determine the required page size. @@ -142,10 +143,10 @@ class GenericAPIView(views.APIView):          if not self.allow_empty:              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 '                  '`get_queryset()` and explicitly raise a 404 on empty querysets.', -                PendingDeprecationWarning, stacklevel=2 +                DeprecationWarning, stacklevel=2              )          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."))          try:              page = paginator.page(page_number) -        except InvalidPage as e: -            raise Http404(_('Invalid page (%(page_number)s): %(message)s') % { -                                'page_number': page_number, -                                'message': str(e) +        except InvalidPage as exc: +            error_format = _('Invalid page (%(page_number)s): %(message)s') +            raise Http404(error_format % { +                'page_number': page_number, +                'message': str(exc)              })          if deprecated_style: @@ -199,19 +201,17 @@ class GenericAPIView(views.APIView):          if not filter_backends and self.filter_backend:              warnings.warn(                  '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 '                  'a *list* of filter backend classes.', -                PendingDeprecationWarning, stacklevel=2 +                DeprecationWarning, stacklevel=2              )              filter_backends = [self.filter_backend]          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):          """ @@ -224,8 +224,8 @@ class GenericAPIView(views.APIView):          """          if queryset is not None:              warnings.warn('The `queryset` parameter to `get_paginate_by()` ' -                          'is due to be deprecated.', -                          PendingDeprecationWarning, stacklevel=2) +                          'is deprecated.', +                          DeprecationWarning, stacklevel=2)          if self.paginate_by_param:              try: @@ -284,8 +284,8 @@ class GenericAPIView(views.APIView):          if self.model is not None:              return self.model._default_manager.all() -        raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" -                                    % self.__class__.__name__) +        error_format = "'%s' must define 'queryset' or 'model'" +        raise ImproperlyConfigured(error_format % self.__class__.__name__)      def get_object(self, queryset=None):          """ @@ -312,16 +312,16 @@ class GenericAPIView(views.APIView):              filter_kwargs = {self.lookup_field: lookup}          elif pk is not None and self.lookup_field == 'pk':              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', -                PendingDeprecationWarning +                DeprecationWarning              )              filter_kwargs = {'pk': pk}          elif slug is not None and self.lookup_field == 'pk':              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', -                PendingDeprecationWarning +                DeprecationWarning              )              filter_kwargs = {self.slug_field: slug}          else: @@ -339,12 +339,11 @@ class GenericAPIView(views.APIView):          return obj -    ######################## -    ### The following are placeholder methods, -    ### and are intended to be overridden. -    ### -    ### The are not called by GenericAPIView directly, -    ### but are used by the mixin methods. +    # The following are placeholder methods, +    # and are intended to be overridden. +    # +    # The are not called by GenericAPIView directly, +    # but are used by the mixin methods.      def pre_save(self, obj):          """ @@ -416,10 +415,8 @@ class GenericAPIView(views.APIView):          return ret -########################################################## -### Concrete view classes that provide method handlers ### -### by composing the mixin classes with the base view. ### -########################################################## +# Concrete view classes that provide method handlers +# by composing the mixin classes with the base view.  class CreateAPIView(mixins.CreateModelMixin,                      GenericAPIView): @@ -534,16 +531,14 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,          return self.destroy(request, *args, **kwargs) -########################## -### Deprecated classes ### -########################## +# Deprecated classes  class MultipleObjectAPIView(GenericAPIView):      def __init__(self, *args, **kwargs):          warnings.warn( -            'Subclassing `MultipleObjectAPIView` is due to be deprecated. ' +            'Subclassing `MultipleObjectAPIView` is deprecated. '              'You should simply subclass `GenericAPIView` instead.', -            PendingDeprecationWarning, stacklevel=2 +            DeprecationWarning, stacklevel=2          )          super(MultipleObjectAPIView, self).__init__(*args, **kwargs) @@ -551,8 +546,8 @@ class MultipleObjectAPIView(GenericAPIView):  class SingleObjectAPIView(GenericAPIView):      def __init__(self, *args, **kwargs):          warnings.warn( -            'Subclassing `SingleObjectAPIView` is due to be deprecated. ' +            'Subclassing `SingleObjectAPIView` is deprecated. '              'You should simply subclass `GenericAPIView` instead.', -            PendingDeprecationWarning, stacklevel=2 +            DeprecationWarning, stacklevel=2          )          super(SingleObjectAPIView, self).__init__(*args, **kwargs) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index e1a24dc7..2cc87eef 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -26,14 +26,14 @@ def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None)      include = []      if pk: -        # Pending deprecation +        # Deprecated          pk_field = obj._meta.pk          while pk_field.rel:              pk_field = pk_field.rel.to._meta.pk          include.append(pk_field.name)      if slug_field: -        # Pending deprecation +        # Deprecated          include.append(slug_field)      if lookup_field and lookup_field != 'pk': @@ -79,10 +79,10 @@ class ListModelMixin(object):          # `.allow_empty = False`, to raise 404 errors on empty querysets.          if not self.allow_empty and not self.object_list:              warnings.warn( -                'The `allow_empty` parameter is due to be deprecated. ' +                'The `allow_empty` parameter is deprecated. '                  'To use `allow_empty=False` style behavior, You should override '                  '`get_queryset()` and explicitly raise a 404 on empty querysets.', -                PendingDeprecationWarning +                DeprecationWarning              )              class_name = self.__class__.__name__              error_msg = self.empty_error % {'class_name': class_name} diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index 4d205c0e..ca7b5397 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -54,8 +54,10 @@ class DefaultContentNegotiation(BaseContentNegotiation):                  for media_type in media_type_set:                      if media_type_matches(renderer.media_type, media_type):                          # Return the most specific media type as accepted. -                        if (_MediaType(renderer.media_type).precedence > -                            _MediaType(media_type).precedence): +                        if ( +                            _MediaType(renderer.media_type).precedence > +                            _MediaType(media_type).precedence +                        ):                              # Eg client requests '*/*'                              # Accepted media type is 'application/json'                              return renderer, renderer.media_type diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 4990971b..aa4fd3f1 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -10,7 +10,8 @@ from django.core.files.uploadhandler import StopFutureHandlers  from django.http import QueryDict  from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser  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 import renderers  import json diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index f24a5123..6a1a0077 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -2,15 +2,12 @@  Provides a set of pluggable permission policies.  """  from __future__ import unicode_literals -import inspect -import warnings - -SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] -  from django.http import Http404  from rest_framework.compat import (get_model_name, oauth2_provider_scope,                                     oauth2_constants) +SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] +  class BasePermission(object):      """ @@ -27,13 +24,6 @@ class BasePermission(object):          """          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 @@ -72,9 +62,11 @@ class IsAuthenticatedOrReadOnly(BasePermission):      """      def has_permission(self, request, view): -        return (request.method in SAFE_METHODS or  -            request.user and  -            request.user.is_authenticated()) +        return ( +            request.method in SAFE_METHODS or +            request.user and +            request.user.is_authenticated() +        )  class DjangoModelPermissions(BasePermission): @@ -132,9 +124,11 @@ class DjangoModelPermissions(BasePermission):          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.has_perms(perms)) +            request.user.has_perms(perms) +        )  class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): @@ -222,6 +216,8 @@ class TokenHasReadWriteScope(BasePermission):              required = oauth2_constants.READ if read_only else oauth2_constants.WRITE              return oauth2_provider_scope.check(required, request.auth.scope) -        assert False, ('TokenHasReadWriteScope requires either the' -        '`OAuthAuthentication` or `OAuth2Authentication` authentication ' -        'class to be used.') +        assert False, ( +            'TokenHasReadWriteScope requires either the' +            '`OAuthAuthentication` or `OAuth2Authentication` authentication ' +            'class to be used.' +        ) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 3463954d..1acbdce2 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -19,8 +19,7 @@ from rest_framework.compat import smart_text  import warnings -##### Relational fields ##### - +# Relational fields  # Not actually Writable, but subclasses may need to be.  class RelatedField(WritableField): @@ -41,14 +40,6 @@ class RelatedField(WritableField):      many = False      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)          self.many = kwargs.pop('many', self.many)          if self.many: @@ -74,7 +65,7 @@ class RelatedField(WritableField):              else:  # Reverse                  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):          return self.to_native(obj) @@ -121,7 +112,7 @@ class RelatedField(WritableField):      choices = property(_get_choices, _set_choices) -    ### Default value handling +    # Default value handling      def get_default_value(self):          default = super(RelatedField, self).get_default_value() @@ -129,7 +120,7 @@ class RelatedField(WritableField):              return []          return default -    ### Regular serializer stuff... +    # Regular serializer stuff...      def field_to_native(self, obj, field_name):          try: @@ -189,7 +180,7 @@ class RelatedField(WritableField):              into[(self.source or field_name)] = self.from_native(value) -### PrimaryKey relationships +# PrimaryKey relationships  class PrimaryKeyRelatedField(RelatedField):      """ @@ -277,8 +268,7 @@ class PrimaryKeyRelatedField(RelatedField):          return self.to_native(pk) -### Slug relationships - +# Slug relationships  class SlugRelatedField(RelatedField):      """ @@ -313,7 +303,7 @@ class SlugRelatedField(RelatedField):              raise ValidationError(msg) -### Hyperlinked relationships +# Hyperlinked relationships  class HyperlinkedRelatedField(RelatedField):      """ @@ -330,7 +320,7 @@ class HyperlinkedRelatedField(RelatedField):          'incorrect_type': _('Incorrect type.  Expected url string, received %s.'),      } -    # These are all pending deprecation +    # These are all deprecated      pk_url_kwarg = 'pk'      slug_field = 'slug'      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.format = kwargs.pop('format', None) -        # These are pending deprecation +        # These are deprecated          if 'pk_url_kwarg' in kwargs: -            msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.' -            warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) +            msg = 'pk_url_kwarg is deprecated. Use lookup_field instead.' +            warnings.warn(msg, DeprecationWarning, stacklevel=2)          if 'slug_url_kwarg' in kwargs: -            msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.' -            warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) +            msg = 'slug_url_kwarg is deprecated. Use lookup_field instead.' +            warnings.warn(msg, DeprecationWarning, stacklevel=2)          if 'slug_field' in kwargs: -            msg = 'slug_field is pending deprecation. Use lookup_field instead.' -            warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) +            msg = 'slug_field is deprecated. Use lookup_field instead.' +            warnings.warn(msg, DeprecationWarning, stacklevel=2)          self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)          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,                      # then `slug_field` is being used implicitly, and we                      # 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.' -                    warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) +                    warnings.warn(msg, DeprecationWarning, stacklevel=2)                  return ret              except NoReverseMatch:                  pass @@ -432,14 +422,11 @@ class HyperlinkedRelatedField(RelatedField):          request = self.context.get('request', None)          format = self.format or self.context.get('format', None) -        if request is None: -            msg = ( -                "Using `HyperlinkedRelatedField` without including the request " -                "in the serializer context is deprecated. " -                "Add `context={'request': request}` when instantiating " -                "the serializer." -            ) -            warnings.warn(msg, DeprecationWarning, stacklevel=4) +        assert request is not None, ( +            "`HyperlinkedRelatedField` requires the request in the serializer " +            "context. Add `context={'request': request}` when instantiating " +            "the serializer." +        )          # If the object has not yet been saved then we cannot hyperlink to it.          if getattr(obj, 'pk', None) is None: @@ -499,7 +486,7 @@ class HyperlinkedIdentityField(Field):      lookup_field = 'pk'      read_only = True -    # These are all pending deprecation +    # These are all deprecated      pk_url_kwarg = 'pk'      slug_field = 'slug'      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)          self.lookup_field = lookup_field or self.lookup_field -        # These are pending deprecation +        # These are deprecated          if 'pk_url_kwarg' in kwargs: -            msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.' -            warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) +            msg = 'pk_url_kwarg is deprecated. Use lookup_field instead.' +            warnings.warn(msg, DeprecationWarning, stacklevel=2)          if 'slug_url_kwarg' in kwargs: -            msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.' -            warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) +            msg = 'slug_url_kwarg is deprecated. Use lookup_field instead.' +            warnings.warn(msg, DeprecationWarning, stacklevel=2)          if 'slug_field' in kwargs: -            msg = 'slug_field is pending deprecation. Use lookup_field instead.' -            warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) +            msg = 'slug_field is deprecated. Use lookup_field instead.' +            warnings.warn(msg, DeprecationWarning, stacklevel=2)          self.slug_field = kwargs.pop('slug_field', self.slug_field)          default_slug_kwarg = self.slug_url_kwarg or self.slug_field @@ -538,11 +525,11 @@ class HyperlinkedIdentityField(Field):          format = self.context.get('format', None)          view_name = self.view_name -        if request is None: -            warnings.warn("Using `HyperlinkedIdentityField` without including the " -                          "request in the serializer context is deprecated. " -                          "Add `context={'request': request}` when instantiating the serializer.", -                          DeprecationWarning, stacklevel=4) +        assert request is not None, ( +            "`HyperlinkedIdentityField` requires the request in the serializer" +            " context. Add `context={'request': request}` when instantiating " +            "the serializer." +        )          # By default use whatever format is given for the current context          # unless the target is a different type to the source. @@ -606,41 +593,3 @@ class HyperlinkedIdentityField(Field):                  pass          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) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 7048d87d..748ebac9 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -8,7 +8,6 @@ REST framework also provides an HTML renderer the renders the browsable API.  """  from __future__ import unicode_literals -import copy  import json  import django  from django import forms @@ -16,11 +15,9 @@ from django.core.exceptions import ImproperlyConfigured  from django.http.multipartparser import parse_header  from django.template import RequestContext, loader, Template  from django.test.client import encode_multipart +from django.utils import six  from django.utils.xmlutils import SimplerXMLGenerator -from rest_framework.compat import StringIO -from rest_framework.compat import six -from rest_framework.compat import smart_text -from rest_framework.compat import yaml +from rest_framework.compat import StringIO, smart_text, yaml  from rest_framework.exceptions import ParseError  from rest_framework.settings import api_settings  from rest_framework.request import is_form_media_type, override_method @@ -75,7 +72,6 @@ class JSONRenderer(BaseRenderer):          # E.g. If we're being called by the BrowsableAPIRenderer.          return renderer_context.get('indent', None) -      def render(self, data, accepted_media_type=None, renderer_context=None):          """          Render `data` into JSON, returning a bytestring. @@ -86,8 +82,10 @@ class JSONRenderer(BaseRenderer):          renderer_context = renderer_context or {}          indent = self.get_indent(accepted_media_type, renderer_context) -        ret = json.dumps(data, cls=self.encoder_class, -            indent=indent, ensure_ascii=self.ensure_ascii) +        ret = json.dumps( +            data, cls=self.encoder_class, +            indent=indent, ensure_ascii=self.ensure_ascii +        )          # On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,          # 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.          """ -        if not method in view.allowed_methods: +        if method not in view.allowed_methods:              return  # Not a valid method          if not api_settings.FORM_METHOD_OVERRIDE: @@ -454,8 +452,10 @@ class BrowsableAPIRenderer(BaseRenderer):              if method in ('DELETE', 'OPTIONS'):                  return True  # Don't actually need to return a form -            if (not getattr(view, 'get_serializer', None) -                or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)): +            if ( +                not getattr(view, 'get_serializer', None) +                or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes) +            ):                  return              serializer = view.get_serializer(instance=obj, data=data, files=files) @@ -576,7 +576,7 @@ class BrowsableAPIRenderer(BaseRenderer):              'version': VERSION,              'breadcrumblist': self.get_breadcrumbs(request),              '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,              '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):          return encode_multipart(self.BOUNDARY, data) - diff --git a/rest_framework/request.py b/rest_framework/request.py index d508f9b4..27532661 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -295,8 +295,11 @@ class Request(object):          Return the content body of the request, as a stream.          """          try: -            content_length = int(self.META.get('CONTENT_LENGTH', -                                    self.META.get('HTTP_CONTENT_LENGTH'))) +            content_length = int( +                self.META.get( +                    'CONTENT_LENGTH', self.META.get('HTTP_CONTENT_LENGTH') +                ) +            )          except (ValueError, TypeError):              content_length = 0 @@ -320,9 +323,11 @@ class Request(object):          )          # 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 not is_form_media_type(self._content_type)): +            or not is_form_media_type(self._content_type) +        ):              return          # 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          # Method overloading - change the method and remove the param from the content. -        if (self._METHOD_PARAM and -            self._METHOD_PARAM in self._data): +        if ( +            self._METHOD_PARAM and +            self._METHOD_PARAM in self._data +        ):              self._method = self._data[self._METHOD_PARAM].upper()          # 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._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._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))              self._data, self._files = (Empty, Empty) @@ -394,7 +403,7 @@ class Request(object):                  self._not_authenticated()                  raise -            if not user_auth_tuple is None: +            if user_auth_tuple is not None:                  self._authenticator = authenticator                  self._user, self._auth = user_auth_tuple                  return diff --git a/rest_framework/response.py b/rest_framework/response.py index 25b78524..0a7d313f 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -8,7 +8,7 @@ from __future__ import unicode_literals  import django  from django.core.handlers.wsgi import STATUS_CODE_TEXT  from django.template.response import SimpleTemplateResponse -from rest_framework.compat import six +from django.utils import six  class Response(SimpleTemplateResponse): @@ -62,8 +62,10 @@ class Response(SimpleTemplateResponse):          ret = renderer.render(self.data, media_type, context)          if isinstance(ret, six.text_type): -            assert charset, 'renderer returned unicode, and did not specify ' \ -            'a charset value.' +            assert charset, ( +                'renderer returned unicode, and did not specify ' +                'a charset value.' +            )              return bytes(ret.encode(charset))          if not ret: diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 97b35c10..406ebcf7 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -17,15 +17,17 @@ from __future__ import unicode_literals  import itertools  from collections import namedtuple +from django.conf.urls import patterns, url  from django.core.exceptions import ImproperlyConfigured  from rest_framework import views -from rest_framework.compat import patterns, url  from rest_framework.response import Response  from rest_framework.reverse import reverse  from rest_framework.urlpatterns import format_suffix_patterns  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): @@ -88,6 +90,14 @@ class SimpleRouter(BaseRouter):              name='{basename}-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.          Route(              url=r'^{prefix}/{lookup}{trailing_slash}$', @@ -100,13 +110,10 @@ class SimpleRouter(BaseRouter):              name='{basename}-detail',              initkwargs={'suffix': 'Instance'}          ), -        # Dynamically generated routes. -        # Generated using @action or @link decorators on methods of the viewset. -        Route( +        # Dynamically generated detail routes. +        # Generated using @detail_route decorator on methods of the viewset. +        DynamicDetailRoute(              url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$', -            mapping={ -                '{httpmethod}': '{methodname}', -            },              name='{basename}-{methodnamehyphen}',              initkwargs={}          ), @@ -139,25 +146,42 @@ class SimpleRouter(BaseRouter):          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 -        dynamic_routes = [] +        # Determine any `@detail_route` or `@list_route` decorated methods on the viewset +        detail_routes = [] +        list_routes = []          for methodname in dir(viewset):              attr = getattr(viewset, methodname)              httpmethods = getattr(attr, 'bind_to_methods', None) +            detail = getattr(attr, 'detail', True)              if httpmethods:                  if methodname in known_actions: -                    raise ImproperlyConfigured('Cannot use @action or @link decorator on ' -                                               'method "%s" as it is an existing route' % methodname) +                    raise ImproperlyConfigured('Cannot use @detail_route or @list_route ' +                                               'decorators on method "%s" ' +                                               'as it is an existing route' % methodname)                  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 = []          for route in self.routes: -            if route.mapping == {'{httpmethod}': '{methodname}'}: -                # Dynamic routes (@link or @action decorator) -                for httpmethods, methodname in dynamic_routes: +            if isinstance(route, DynamicDetailRoute): +                # Dynamic detail routes (@detail_route decorator) +                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.update(getattr(viewset, methodname).kwargs)                      ret.append(Route( @@ -195,13 +219,16 @@ class SimpleRouter(BaseRouter):          https://github.com/alanjds/drf-nested-routers          """ -        if self.trailing_slash: -            base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/]+)' -        else: -            # Don't consume `.json` style suffixes -            base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/.]+)' +        base_regex = '(?P<{lookup_prefix}{lookup_field}>{lookup_value})' +        # Use `pk` as default field, unset set.  Default regex should not +        # consume `.json` style suffixes and should break at '/' boundaries.          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):          """ diff --git a/rest_framework/runtests/runcoverage.py b/rest_framework/runtests/runcoverage.py deleted file mode 100755 index ce11b213..00000000 --- a/rest_framework/runtests/runcoverage.py +++ /dev/null @@ -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() diff --git a/rest_framework/runtests/runtests.py b/rest_framework/runtests/runtests.py deleted file mode 100755 index 2daaae4e..00000000 --- a/rest_framework/runtests/runtests.py +++ /dev/null @@ -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() diff --git a/rest_framework/runtests/urls.py b/rest_framework/runtests/urls.py deleted file mode 100644 index ed5baeae..00000000 --- a/rest_framework/runtests/urls.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Blank URLConf just to keep runtests.py happy. -""" -from rest_framework.compat import patterns - -urlpatterns = patterns('', -) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 43d339da..be8ad3f2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -20,9 +20,9 @@ from django.contrib.contenttypes.generic import GenericForeignKey  from django.core.paginator import Page  from django.db import models  from django.forms import widgets +from django.utils import six  from django.utils.datastructures import SortedDict  from django.core.exceptions import ObjectDoesNotExist -from rest_framework.compat import get_concrete_model, six  from rest_framework.settings import api_settings @@ -182,7 +182,7 @@ class BaseSerializer(WritableField):      _dict_class = SortedDictWithMetadata      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):          super(BaseSerializer, self).__init__(**kwargs)          self.opts = self._options_class(self.Meta) @@ -412,12 +412,7 @@ class BaseSerializer(WritableField):          if value is None:              return None -        if self.many is not None: -            many = self.many -        else: -            many = hasattr(value, '__iter__') and not isinstance(value, (Page, dict, six.text_type)) - -        if many: +        if self.many:              return [self.to_native(item) for item in 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                  # to iterate through each instance. -                if (self.many and +                if ( +                    self.many and                      not hasattr(obj, '__iter__') and -                    is_simple_callable(getattr(obj, 'all', None))): +                    is_simple_callable(getattr(obj, 'all', None)) +                ):                      obj = obj.all()                  kwargs = { @@ -606,8 +603,10 @@ class BaseSerializer(WritableField):          API schemas for auto-documentation.          """          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 -        assert cls is not None, \ -                "Serializer class '%s' is missing 'model' Meta option" % self.__class__.__name__ -        opts = get_concrete_model(cls)._meta +        assert cls is not None, ( +            "Serializer class '%s' is missing 'model' Meta option" % +            self.__class__.__name__ +        ) +        opts = cls._meta.concrete_model._meta          ret = SortedDict()          nested = bool(self.opts.depth) @@ -673,9 +674,9 @@ class ModelSerializer(Serializer):              # If model is a child via multitable inheritance, use parent's pk              pk_field = pk_field.rel.to._meta.pk -        field = self.get_pk_field(pk_field) -        if field: -            ret[pk_field.name] = field +        serializer_pk_field = self.get_pk_field(pk_field) +        if serializer_pk_field: +            ret[pk_field.name] = serializer_pk_field          # Deal with forward relationships          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:                      warnings.warn(                          'The `get_nested_field(model_field)` call signature ' -                        'is due to be deprecated. ' +                        'is deprecated. '                          'Use `get_nested_field(model_field, related_model, '                          'to_many) instead', -                        PendingDeprecationWarning +                        DeprecationWarning                      )                      field = self.get_nested_field(model_field)                  else: @@ -708,10 +709,10 @@ class ModelSerializer(Serializer):                  if len(inspect.getargspec(self.get_nested_field).args) == 3:                      warnings.warn(                          '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, '                          'to_many) instead', -                        PendingDeprecationWarning +                        DeprecationWarning                      )                      field = self.get_related_field(model_field, to_many=to_many)                  else: @@ -744,9 +745,11 @@ class ModelSerializer(Serializer):              is_m2m = isinstance(relation.field,                                  models.fields.related.ManyToManyField) -            if (is_m2m and +            if ( +                is_m2m 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              if nested: @@ -881,6 +884,10 @@ class ModelSerializer(Serializer):                  issubclass(model_field.__class__, models.PositiveSmallIntegerField):              kwargs['min_value'] = 0 +        if model_field.null and \ +                issubclass(model_field.__class__, (models.CharField, models.TextField)): +            kwargs['allow_none'] = True +          attribute_dict = {              models.CharField: ['max_length'],              models.CommaSeparatedIntegerField: ['max_length'], @@ -907,15 +914,17 @@ class ModelSerializer(Serializer):          Return a list of field names to exclude from model validation.          """          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]          for field_name, field in self.fields.items():              field_name = field.source or field_name -            if field_name in exclusions \ -                and not field.read_only \ -                and (field.required or hasattr(instance, field_name)) \ -                and not isinstance(field, Serializer): +            if ( +                field_name in exclusions +                and not field.read_only +                and (field.required or hasattr(instance, field_name)) +                and not isinstance(field, Serializer) +            ):                  exclusions.remove(field_name)          return exclusions diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 38753c96..644751f8 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -18,12 +18,9 @@ REST framework settings, checking for user settings first, then falling  back to the defaults.  """  from __future__ import unicode_literals -  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.compat import six  USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None) @@ -46,16 +43,12 @@ DEFAULTS = {      'DEFAULT_PERMISSION_CLASSES': (          'rest_framework.permissions.AllowAny',      ), -    'DEFAULT_THROTTLE_CLASSES': ( -    ), -    'DEFAULT_CONTENT_NEGOTIATION_CLASS': -        'rest_framework.negotiation.DefaultContentNegotiation', +    'DEFAULT_THROTTLE_CLASSES': (), +    'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',      # Genric view behavior -    'DEFAULT_MODEL_SERIALIZER_CLASS': -        'rest_framework.serializers.ModelSerializer', -    'DEFAULT_PAGINATION_SERIALIZER_CLASS': -        'rest_framework.pagination.PaginationSerializer', +    'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer', +    'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'rest_framework.pagination.PaginationSerializer',      'DEFAULT_FILTER_BACKENDS': (),      # Throttling @@ -63,6 +56,7 @@ DEFAULTS = {          'user': None,          'anon': None,      }, +    'NUM_PROXIES': None,      # Pagination      'PAGINATE_BY': None, @@ -119,6 +113,7 @@ DEFAULTS = {      # Pending deprecation      'FILTER_BACKEND': None, +  } diff --git a/rest_framework/six.py b/rest_framework/six.py deleted file mode 100644 index 9e382312..00000000 --- a/rest_framework/six.py +++ /dev/null @@ -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")) diff --git a/rest_framework/status.py b/rest_framework/status.py index 76435371..90a75508 100644 --- a/rest_framework/status.py +++ b/rest_framework/status.py @@ -10,15 +10,19 @@ from __future__ import unicode_literals  def is_informational(code):      return code >= 100 and code <= 199 +  def is_success(code):      return code >= 200 and code <= 299 +  def is_redirect(code):      return code >= 300 and code <= 399 +  def is_client_error(code):      return code >= 400 and code <= 499 +  def is_server_error(code):      return code >= 500 and code <= 599 diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index ee96b6ee..b6e9ca5c 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -1,4 +1,5 @@  {% load url from future %} +{% load staticfiles %}  {% load rest_framework %}  <!DOCTYPE html>  <html> diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html index 312a1138..43860e53 100644 --- a/rest_framework/templates/rest_framework/login_base.html +++ b/rest_framework/templates/rest_framework/login_base.html @@ -1,5 +1,6 @@  {% extends "rest_framework/base.html" %}  {% load url from future %} +{% load staticfiles %}  {% load rest_framework %}      {% block body %} diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index a155d8d2..b80a7d77 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -2,98 +2,17 @@ from __future__ import unicode_literals, absolute_import  from django import template  from django.core.urlresolvers import reverse, NoReverseMatch  from django.http import QueryDict +from django.utils import six  from django.utils.encoding import iri_to_uri  from django.utils.html import escape  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  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):      """      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):]                      lead = lead + opening                  # Keep parentheses at the end only if they're balanced. -                if (middle.endswith(closing) -                    and middle.count(closing) == middle.count(opening) + 1): +                if ( +                    middle.endswith(closing) +                    and middle.count(closing) == middle.count(opening) + 1 +                ):                      middle = middle[:-len(closing)]                      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)              elif simple_url_2_re.match(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)                  try:                      domain = domain.encode('idna').decode('ascii') diff --git a/rest_framework/test.py b/rest_framework/test.py index d4ec50a0..f89a6dcd 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -8,10 +8,11 @@ from django.conf import settings  from django.test.client import Client as DjangoClient  from django.test.client import ClientHandler  from django.test import testcases +from django.utils import six  from django.utils.http import urlencode  from rest_framework.settings import api_settings  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): @@ -49,9 +50,10 @@ class APIRequestFactory(DjangoRequestFactory):          else:              format = format or self.default_format -            assert format in self.renderer_classes, ("Invalid format '{0}'. " -                "Available formats are {1}.  Set TEST_REQUEST_RENDERER_CLASSES " -                "to enable extra request formats.".format( +            assert format in self.renderer_classes, ( +                "Invalid format '{0}'. Available formats are {1}. " +                "Set TEST_REQUEST_RENDERER_CLASSES to enable " +                "extra request formats.".format(                      format,                      ', '.join(["'" + fmt + "'" for fmt in self.renderer_classes.keys()])                  ) diff --git a/rest_framework/tests/test_breadcrumbs.py b/rest_framework/tests/test_breadcrumbs.py deleted file mode 100644 index 41ddf2ce..00000000 --- a/rest_framework/tests/test_breadcrumbs.py +++ /dev/null @@ -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', '/')]) diff --git a/rest_framework/tests/tests.py b/rest_framework/tests/tests.py deleted file mode 100644 index 554ebd1a..00000000 --- a/rest_framework/tests/tests.py +++ /dev/null @@ -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) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 91be9cfd..7e9f9d71 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -18,6 +18,25 @@ class BaseThrottle(object):          """          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):          """          Optionally, return a recommended number of seconds to wait before @@ -162,7 +181,7 @@ class AnonRateThrottle(SimpleRateThrottle):          return self.cache_format % {              'scope': self.scope, -            'ident': ident +            'ident': self.get_ident(request)          } @@ -180,7 +199,7 @@ class UserRateThrottle(SimpleRateThrottle):          if request.user.is_authenticated():              ident = request.user.id          else: -            ident = request.META.get('REMOTE_ADDR', None) +            ident = self.get_ident(request)          return self.cache_format % {              'scope': self.scope, @@ -228,7 +247,7 @@ class ScopedRateThrottle(SimpleRateThrottle):          if request.user.is_authenticated():              ident = request.user.id          else: -            ident = request.META.get('REMOTE_ADDR', None) +            ident = self.get_ident(request)          return self.cache_format % {              'scope': self.scope, diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 0ff137b0..038e9ee3 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -1,6 +1,6 @@  from __future__ import unicode_literals +from django.conf.urls import url, include  from django.core.urlresolvers import RegexURLResolver -from rest_framework.compat import url, include  from rest_framework.settings import api_settings diff --git a/rest_framework/urls.py b/rest_framework/urls.py index 5d70f899..8fa3073e 100644 --- a/rest_framework/urls.py +++ b/rest_framework/urls.py @@ -8,17 +8,19 @@ your API requires authentication:          ...          url(r'^auth', include('rest_framework.urls', namespace='rest_framework'))      ) -     +  The urls must be namespaced as 'rest_framework', and you should make sure  your authentication settings include `SessionAuthentication`.  """  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'} -urlpatterns = patterns('django.contrib.auth.views', -    url(r'^login/$', 'login', template_name, name='login'), -    url(r'^logout/$', 'logout', template_name, name='logout'), +urlpatterns = patterns( +    '', +    url(r'^login/$', views.login, template_name, name='login'), +    url(r'^logout/$', views.logout, template_name, name='logout')  ) diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index e5fa4194..00ffdfba 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -2,10 +2,11 @@  Helper classes for parsers.  """  from __future__ import unicode_literals +from django.utils import timezone  from django.db.models.query import QuerySet  from django.utils.datastructures import SortedDict  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  import datetime  import decimal @@ -97,14 +98,23 @@ else:                      node.flow_style = best_style              return node -    SafeDumper.add_representer(decimal.Decimal, -            SafeDumper.represent_decimal) - -    SafeDumper.add_representer(SortedDict, -            yaml.representer.SafeRepresenter.represent_dict) -    SafeDumper.add_representer(DictWithMetadata, -            yaml.representer.SafeRepresenter.represent_dict) -    SafeDumper.add_representer(SortedDictWithMetadata, -            yaml.representer.SafeRepresenter.represent_dict) -    SafeDumper.add_representer(types.GeneratorType, -            yaml.representer.SafeRepresenter.represent_list) +    SafeDumper.add_representer( +        decimal.Decimal, +        SafeDumper.represent_decimal +    ) +    SafeDumper.add_representer( +        SortedDict, +        yaml.representer.SafeRepresenter.represent_dict +    ) +    SafeDumper.add_representer( +        DictWithMetadata, +        yaml.representer.SafeRepresenter.represent_dict +    ) +    SafeDumper.add_representer( +        SortedDictWithMetadata, +        yaml.representer.SafeRepresenter.represent_dict +    ) +    SafeDumper.add_representer( +        types.GeneratorType, +        yaml.representer.SafeRepresenter.represent_list +    ) diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 4b59ba84..6d53aed1 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -6,8 +6,6 @@ from __future__ import unicode_literals  from django.utils.html import escape  from django.utils.safestring import mark_safe  from rest_framework.compat import apply_markdown -from rest_framework.settings import api_settings -from textwrap import dedent  import re @@ -40,6 +38,7 @@ def dedent(content):      return content.strip() +  def camelcase_to_spaces(content):      """      Translate 'CamelCaseNames' to 'Camel Case Names'. @@ -49,6 +48,7 @@ def camelcase_to_spaces(content):      content = re.sub(camelcase_boundry, ' \\1', content).strip()      return ' '.join(content.split('_')).title() +  def markup_description(description):      """      Apply HTML markup to the given description. diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py index 92f99efd..87b3cc6a 100644 --- a/rest_framework/utils/mediatypes.py +++ b/rest_framework/utils/mediatypes.py @@ -57,7 +57,7 @@ class _MediaType(object):              if key != 'q' and other.params.get(key, None) != self.params.get(key, None):                  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          if self.main_type != '*' and other.main_type != '*' and other.main_type != self.main_type: @@ -79,7 +79,7 @@ class _MediaType(object):          return 3      def __str__(self): -        return unicode(self).encode('utf-8') +        return self.__unicode__().encode('utf-8')      def __unicode__(self):          ret = "%s/%s" % (self.main_type, self.sub_type) diff --git a/rest_framework/views.py b/rest_framework/views.py index a2668f2c..bca0aaef 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -31,6 +31,7 @@ def get_view_name(view_cls, suffix=None):      return name +  def get_view_description(view_cls, html=False):      """      Given a view class, return a textual description to represent the view. @@ -119,7 +120,6 @@ class APIView(View):              headers['Vary'] = 'Accept'          return headers -      def http_method_not_allowed(self, request, *args, **kwargs):          """          If `request.method` does not correspond to a handler method, diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 7eb29f99..bb5b304e 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -127,11 +127,11 @@ class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,  class ModelViewSet(mixins.CreateModelMixin, -                    mixins.RetrieveModelMixin, -                    mixins.UpdateModelMixin, -                    mixins.DestroyModelMixin, -                    mixins.ListModelMixin, -                    GenericViewSet): +                   mixins.RetrieveModelMixin, +                   mixins.UpdateModelMixin, +                   mixins.DestroyModelMixin, +                   mixins.ListModelMixin, +                   GenericViewSet):      """      A viewset that provides default `create()`, `retrieve()`, `update()`,      `partial_update()`, `destroy()` and `list()` actions. diff --git a/runtests.py b/runtests.py new file mode 100755 index 00000000..4da05ac3 --- /dev/null +++ b/runtests.py @@ -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)) @@ -2,11 +2,26 @@  # -*- coding: utf-8 -*-  from setuptools import setup +from setuptools.command.test import test as TestCommand  import re  import os  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):      """      Return package version as listed in `__version__` in `init.py`. @@ -62,7 +77,7 @@ setup(      author_email='tom@tomchristie.com',  # SEE NOTE BELOW (*)      packages=get_packages('rest_framework'),      package_data=get_package_data('rest_framework'), -    test_suite='rest_framework.runtests.runtests.main', +    cmdclass={'test': PyTest},      install_requires=[],      classifiers=[          'Development Status :: 5 - Production/Stable', diff --git a/rest_framework/tests/__init__.py b/tests/__init__.py index e69de29b..e69de29b 100644 --- a/rest_framework/tests/__init__.py +++ b/tests/__init__.py diff --git a/rest_framework/tests/accounts/__init__.py b/tests/accounts/__init__.py index e69de29b..e69de29b 100644 --- a/rest_framework/tests/accounts/__init__.py +++ b/tests/accounts/__init__.py diff --git a/rest_framework/tests/accounts/models.py b/tests/accounts/models.py index 525e601b..3bf4a0c3 100644 --- a/rest_framework/tests/accounts/models.py +++ b/tests/accounts/models.py @@ -1,6 +1,6 @@  from django.db import models -from rest_framework.tests.users.models import User +from tests.users.models import User  class Account(models.Model): diff --git a/rest_framework/tests/accounts/serializers.py b/tests/accounts/serializers.py index a27b9ca6..57a91b92 100644 --- a/rest_framework/tests/accounts/serializers.py +++ b/tests/accounts/serializers.py @@ -1,7 +1,7 @@  from rest_framework import serializers -from rest_framework.tests.accounts.models import Account -from rest_framework.tests.users.serializers import UserSerializer +from tests.accounts.models import Account +from tests.users.serializers import UserSerializer  class AccountSerializer(serializers.ModelSerializer): diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..f3723aea --- /dev/null +++ b/tests/conftest.py @@ -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 diff --git a/rest_framework/tests/description.py b/tests/description.py index b46d7f54..b46d7f54 100644 --- a/rest_framework/tests/description.py +++ b/tests/description.py diff --git a/rest_framework/tests/extras/__init__.py b/tests/extras/__init__.py index e69de29b..e69de29b 100644 --- a/rest_framework/tests/extras/__init__.py +++ b/tests/extras/__init__.py diff --git a/rest_framework/tests/extras/bad_import.py b/tests/extras/bad_import.py index 68263d94..68263d94 100644 --- a/rest_framework/tests/extras/bad_import.py +++ b/tests/extras/bad_import.py diff --git a/rest_framework/tests/models.py b/tests/models.py index fba3f8f7..fe064b46 100644 --- a/rest_framework/tests/models.py +++ b/tests/models.py @@ -60,6 +60,18 @@ class ReadOnlyManyToManyModel(RESTFrameworkModel):      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  class Comment(RESTFrameworkModel): @@ -172,10 +184,3 @@ class NullableOneToOneSource(RESTFrameworkModel):  class BasicModelSerializer(serializers.ModelSerializer):      class Meta:          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() diff --git a/rest_framework/tests/records/__init__.py b/tests/records/__init__.py index e69de29b..e69de29b 100644 --- a/rest_framework/tests/records/__init__.py +++ b/tests/records/__init__.py diff --git a/rest_framework/tests/records/models.py b/tests/records/models.py index 76954807..76954807 100644 --- a/rest_framework/tests/records/models.py +++ b/tests/records/models.py diff --git a/rest_framework/tests/serializers.py b/tests/serializers.py index cc943c7d..be7b3772 100644 --- a/rest_framework/tests/serializers.py +++ b/tests/serializers.py @@ -1,6 +1,5 @@  from rest_framework import serializers - -from rest_framework.tests.models import NullableForeignKeySource +from tests.models import NullableForeignKeySource  class NullableFKSourceSerializer(serializers.ModelSerializer): diff --git a/rest_framework/runtests/settings.py b/tests/settings.py index 3fc0eb2f..91c9ed09 100644 --- a/rest_framework/runtests/settings.py +++ b/tests/settings.py @@ -68,7 +68,6 @@ SECRET_KEY = 'u@x-aj9(hoh#rb-^ymf#g2jx_hp0vj7u5#b@ag1n^seu9e!%cy'  TEMPLATE_LOADERS = (      'django.template.loaders.filesystem.Loader',      'django.template.loaders.app_directories.Loader', -#     'django.template.loaders.eggs.Loader',  )  MIDDLEWARE_CLASSES = ( @@ -79,7 +78,7 @@ MIDDLEWARE_CLASSES = (      'django.contrib.messages.middleware.MessageMiddleware',  ) -ROOT_URLCONF = 'urls' +ROOT_URLCONF = 'tests.urls'  TEMPLATE_DIRS = (      # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". @@ -93,22 +92,19 @@ INSTALLED_APPS = (      'django.contrib.sessions',      'django.contrib.sites',      'django.contrib.messages', -    # Uncomment the next line to enable the admin: -    # 'django.contrib.admin', -    # Uncomment the next line to enable admin documentation: -    # 'django.contrib.admindocs', +    'django.contrib.staticfiles',      'rest_framework',      'rest_framework.authtoken', -    'rest_framework.tests', -    'rest_framework.tests.accounts', -    'rest_framework.tests.records', -    'rest_framework.tests.users', +    'tests', +    'tests.accounts', +    'tests.records', +    'tests.users',  )  # OAuth is optional and won't work if there is no oauth_provider & oauth2  try: -    import oauth_provider -    import oauth2 +    import oauth_provider  # NOQA +    import oauth2  # NOQA  except ImportError:      pass  else: @@ -117,7 +113,7 @@ else:      )  try: -    import provider +    import provider  # NOQA  except ImportError:      pass  else: @@ -128,13 +124,13 @@ else:  # guardian is optional  try: -    import guardian +    import guardian  # NOQA  except ImportError:      pass  else:      ANONYMOUS_USER_ID = -1      AUTHENTICATION_BACKENDS = ( -        'django.contrib.auth.backends.ModelBackend', # default +        'django.contrib.auth.backends.ModelBackend',  # default          'guardian.backends.ObjectPermissionBackend',      )      INSTALLED_APPS += ( diff --git a/rest_framework/tests/test_authentication.py b/tests/test_authentication.py index 34bf2910..2b9d73e4 100644 --- a/rest_framework/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -1,8 +1,9 @@  from __future__ import unicode_literals +from django.conf.urls import patterns, url, include  from django.contrib.auth.models import User  from django.http import HttpResponse  from django.test import TestCase -from django.utils import unittest +from django.utils import six, unittest  from django.utils.http import urlencode  from rest_framework import HTTP_HEADER_ENCODING  from rest_framework import exceptions @@ -19,7 +20,6 @@ from rest_framework.authentication import (      OAuth2Authentication  )  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 oauth, oauth_provider  from rest_framework.test import APIRequestFactory, APIClient @@ -44,32 +44,45 @@ class MockView(APIView):          return HttpResponse({'a': 1, 'b': 2, 'c': 3}) -urlpatterns = patterns('', +urlpatterns = patterns( +    '',      (r'^session/$', MockView.as_view(authentication_classes=[SessionAuthentication])),      (r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])),      (r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])),      (r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),      (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):      allow_query_params_token = True  if oauth2_provider is not None: -    urlpatterns += patterns('', +    urlpatterns += patterns( +        '',          url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),          url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])),          url(r'^oauth2-test-debug/$', MockView.as_view(authentication_classes=[OAuth2AuthenticationDebug])), -        url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication], -            permission_classes=[permissions.TokenHasReadWriteScope])), +        url( +            r'^oauth2-with-scope-test/$', +            MockView.as_view( +                authentication_classes=[OAuth2Authentication], +                permission_classes=[permissions.TokenHasReadWriteScope] +            ) +        )      )  class BasicAuthTests(TestCase):      """Basic authentication""" -    urls = 'rest_framework.tests.test_authentication' +    urls = 'tests.test_authentication'      def setUp(self):          self.csrf_client = APIClient(enforce_csrf_checks=True) @@ -108,7 +121,7 @@ class BasicAuthTests(TestCase):  class SessionAuthTests(TestCase):      """User session authentication""" -    urls = 'rest_framework.tests.test_authentication' +    urls = 'tests.test_authentication'      def setUp(self):          self.csrf_client = APIClient(enforce_csrf_checks=True) @@ -155,7 +168,7 @@ class SessionAuthTests(TestCase):  class TokenAuthTests(TestCase):      """Token authentication""" -    urls = 'rest_framework.tests.test_authentication' +    urls = 'tests.test_authentication'      def setUp(self):          self.csrf_client = APIClient(enforce_csrf_checks=True) @@ -255,7 +268,7 @@ class IncorrectCredentialsTests(TestCase):  class OAuthTests(TestCase):      """OAuth 1.0a authentication""" -    urls = 'rest_framework.tests.test_authentication' +    urls = 'tests.test_authentication'      def setUp(self):          # 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_SECRET = "token_secret" -        self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET, -            name='example', user=self.user, status=self.consts.ACCEPTED) +        self.consumer = Consumer.objects.create( +            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.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, scope=self.scope, -            token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET, is_approved=True +        self.token = OAuthToken.objects.create( +            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): @@ -485,7 +502,7 @@ class OAuthTests(TestCase):  class OAuth2Tests(TestCase):      """OAuth 2.0 authentication""" -    urls = 'rest_framework.tests.test_authentication' +    urls = 'tests.test_authentication'      def setUp(self):          self.csrf_client = APIClient(enforce_csrf_checks=True) @@ -500,24 +517,24 @@ class OAuth2Tests(TestCase):          self.REFRESH_TOKEN = "refresh_token"          self.oauth2_client = oauth2_provider.oauth2.models.Client.objects.create( -                client_id=self.CLIENT_ID, -                client_secret=self.CLIENT_SECRET, -                redirect_uri='', -                client_type=0, -                name='example', -                user=None, -            ) +            client_id=self.CLIENT_ID, +            client_secret=self.CLIENT_SECRET, +            redirect_uri='', +            client_type=0, +            name='example', +            user=None, +        )          self.access_token = oauth2_provider.oauth2.models.AccessToken.objects.create( -                token=self.ACCESS_TOKEN, -                client=self.oauth2_client, -                user=self.user, -            ) +            token=self.ACCESS_TOKEN, +            client=self.oauth2_client, +            user=self.user, +        )          self.refresh_token = oauth2_provider.oauth2.models.RefreshToken.objects.create( -                user=self.user, -                access_token=self.access_token, -                client=self.oauth2_client -            ) +            user=self.user, +            access_token=self.access_token, +            client=self.oauth2_client +        )      def _create_authorization_header(self, token=None):          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')      def test_post_form_passing_auth_url_transport(self):          """Ensure GETing form over OAuth with correct client credentials in form data succeed""" -        response = self.csrf_client.post('/oauth2-test/', -                data={'access_token': self.access_token.token}) +        response = self.csrf_client.post( +            '/oauth2-test/', +            data={'access_token': self.access_token.token} +        )          self.assertEqual(response.status_code, 200)      @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') diff --git a/tests/test_breadcrumbs.py b/tests/test_breadcrumbs.py new file mode 100644 index 00000000..780fd5c4 --- /dev/null +++ b/tests/test_breadcrumbs.py @@ -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', '/')] +        ) diff --git a/rest_framework/tests/test_decorators.py b/tests/test_decorators.py index 195f0ba3..195f0ba3 100644 --- a/rest_framework/tests/test_decorators.py +++ b/tests/test_decorators.py diff --git a/rest_framework/tests/test_description.py b/tests/test_description.py index 4c03c1de..1e481f06 100644 --- a/rest_framework/tests/test_description.py +++ b/tests/test_description.py @@ -4,8 +4,8 @@ from __future__ import unicode_literals  from django.test import TestCase  from rest_framework.compat import apply_markdown, smart_text  from rest_framework.views import APIView -from rest_framework.tests.description import ViewWithNonASCIICharactersInDocstring -from rest_framework.tests.description import UTF8_TEST_DOCSTRING +from .description import ViewWithNonASCIICharactersInDocstring +from .description import UTF8_TEST_DOCSTRING  # We check that docstrings get nicely un-indented.  DESCRIPTION = """an example docstring diff --git a/rest_framework/tests/test_fields.py b/tests/test_fields.py index 17d12f23..094ac1eb 100644 --- a/rest_framework/tests/test_fields.py +++ b/tests/test_fields.py @@ -12,7 +12,7 @@ from django.db import models  from django.test import TestCase  from django.utils.datastructures import SortedDict  from rest_framework import serializers -from rest_framework.tests.models import RESTFrameworkModel +from tests.models import RESTFrameworkModel  class TimestampedModel(models.Model): @@ -648,7 +648,7 @@ class DecimalFieldTest(TestCase):          s = DecimalSerializer(data={'decimal_field': '123'})          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):          """ @@ -660,7 +660,7 @@ class DecimalFieldTest(TestCase):          s = DecimalSerializer(data={'decimal_field': '99'})          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):          """ @@ -672,7 +672,7 @@ class DecimalFieldTest(TestCase):          s = DecimalSerializer(data={'decimal_field': '123.456'})          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):          """ @@ -684,7 +684,7 @@ class DecimalFieldTest(TestCase):          s = DecimalSerializer(data={'decimal_field': '123.4567'})          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):          """ @@ -696,7 +696,7 @@ class DecimalFieldTest(TestCase):          s = DecimalSerializer(data={'decimal_field': '12345.6'})          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): @@ -729,7 +729,7 @@ class ChoiceFieldTests(TestCase):      def test_invalid_choice_model(self):          s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})          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'], '')      def test_empty_choice_model(self): @@ -875,7 +875,7 @@ class SlugFieldTests(TestCase):          s = SlugFieldSerializer(data={'slug_field': 'a b'})          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): diff --git a/rest_framework/tests/test_files.py b/tests/test_files.py index 78f4cf42..de4f71d1 100644 --- a/rest_framework/tests/test_files.py +++ b/tests/test_files.py @@ -1,8 +1,8 @@  from __future__ import unicode_literals  from django.test import TestCase +from django.utils import six  from rest_framework import serializers  from rest_framework.compat import BytesIO -from rest_framework.compat import six  import datetime @@ -85,11 +85,8 @@ class FileSerializerTests(TestCase):          """          Validation should still function when no data dictionary is provided.          """ -        now = datetime.datetime.now() -        file = BytesIO(six.b('stuff')) -        file.name = 'stuff.txt' -        file.size = len(file.getvalue()) -        uploaded_file = UploadedFile(file=file, created=now) - -        serializer = UploadedFileSerializer(files={'file': file}) +        uploaded_file = BytesIO(six.b('stuff')) +        uploaded_file.name = 'stuff.txt' +        uploaded_file.size = len(uploaded_file.getvalue()) +        serializer = UploadedFileSerializer(files={'file': uploaded_file})          self.assertFalse(serializer.is_valid()) diff --git a/rest_framework/tests/test_filters.py b/tests/test_filters.py index 23226bbc..47bffd43 100644 --- a/rest_framework/tests/test_filters.py +++ b/tests/test_filters.py @@ -5,12 +5,11 @@ from django.db import models  from django.core.urlresolvers import reverse  from django.test import TestCase  from django.utils import unittest +from django.conf.urls import patterns, url  from rest_framework import generics, serializers, status, filters -from rest_framework.compat import django_filters, patterns, url -from rest_framework.settings import api_settings +from rest_framework.compat import django_filters  from rest_framework.test import APIRequestFactory -from rest_framework.tests.models import BasicModel -from .models import FilterableItem +from .models import BaseFilterableItem, FilterableItem, BasicModel  from .utils import temporary_setting  factory = APIRequestFactory() @@ -56,6 +55,18 @@ if django_filters:          filter_class = SeveralFieldsFilter          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      class FilterableItemSerializer(serializers.ModelSerializer):          class Meta: @@ -75,7 +86,8 @@ if django_filters:          def get_queryset(self):              return FilterableItem.objects.all() -    urlpatterns = patterns('', +    urlpatterns = patterns( +        '',          url(r'^(?P<pk>\d+)/$', FilterClassDetailView.as_view(), name='detail-view'),          url(r'^$', FilterClassRootView.as_view(), name='root-view'),          url(r'^get-queryset/$', GetQuerysetView.as_view(), @@ -227,6 +239,18 @@ class IntegrationTestFiltering(CommonFilteringTestCase):          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')      def test_unknown_filter(self):          """          GET requests with filters that aren't configured should return 200. @@ -243,7 +267,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase):      """      Integration tests for filtered detail views.      """ -    urls = 'rest_framework.tests.test_filters' +    urls = 'tests.test_filters'      def _get_url(self, item):          return reverse('detail-view', kwargs=dict(pk=item.pk)) @@ -654,8 +678,8 @@ class SensitiveOrderingFilterTests(TestCase):              self.assertEqual(                  response.data,                  [ -                    {'id': 1, username_field: 'userA'}, # PassB -                    {'id': 2, username_field: 'userB'}, # PassC -                    {'id': 3, username_field: 'userC'}, # PassA +                    {'id': 1, username_field: 'userA'},  # PassB +                    {'id': 2, username_field: 'userB'},  # PassC +                    {'id': 3, username_field: 'userC'},  # PassA                  ]              ) diff --git a/rest_framework/tests/test_genericrelations.py b/tests/test_genericrelations.py index 46a2d863..95295eaa 100644 --- a/rest_framework/tests/test_genericrelations.py +++ b/tests/test_genericrelations.py @@ -84,7 +84,7 @@ class TestGenericRelations(TestCase):                  exclude = ('content_type', 'object_id')          class BookmarkSerializer(serializers.ModelSerializer): -            tags = TagSerializer() +            tags = TagSerializer(many=True)              class Meta:                  model = Bookmark @@ -117,18 +117,18 @@ class TestGenericRelations(TestCase):          serializer = TagSerializer(Tag.objects.all(), many=True)          expected = [ -        { -            'tag': 'django', -            'tagged_item': 'Bookmark: https://www.djangoproject.com/' -        }, -        { -            'tag': 'python', -            'tagged_item': 'Bookmark: https://www.djangoproject.com/' -        }, -        { -            'tag': 'reminder', -            'tagged_item': 'Note: Remember the milk' -        } +            { +                'tag': 'django', +                'tagged_item': 'Bookmark: https://www.djangoproject.com/' +            }, +            { +                'tag': 'python', +                'tagged_item': 'Bookmark: https://www.djangoproject.com/' +            }, +            { +                'tag': 'reminder', +                'tagged_item': 'Note: Remember the milk' +            }          ]          self.assertEqual(serializer.data, expected) diff --git a/rest_framework/tests/test_generics.py b/tests/test_generics.py index 57d327cc..e9f5bebd 100644 --- a/rest_framework/tests/test_generics.py +++ b/tests/test_generics.py @@ -2,11 +2,11 @@ from __future__ import unicode_literals  from django.db import models  from django.shortcuts import get_object_or_404  from django.test import TestCase +from django.utils import six  from rest_framework import generics, renderers, serializers, status  from rest_framework.test import APIRequestFactory -from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel -from rest_framework.tests.models import ForeignKeySource, ForeignKeyTarget -from rest_framework.compat import six +from tests.models import BasicModel, Comment, SlugBasedModel +from tests.models import ForeignKeySource, ForeignKeyTarget  factory = APIRequestFactory() diff --git a/rest_framework/tests/test_htmlrenderer.py b/tests/test_htmlrenderer.py index 514d9e2b..2edc6b4b 100644 --- a/rest_framework/tests/test_htmlrenderer.py +++ b/tests/test_htmlrenderer.py @@ -1,15 +1,15 @@  from __future__ import unicode_literals  from django.core.exceptions import PermissionDenied +from django.conf.urls import patterns, url  from django.http import Http404  from django.test import TestCase  from django.template import TemplateDoesNotExist, Template -import django.template.loader +from django.utils import six  from rest_framework import status -from rest_framework.compat import patterns, url  from rest_framework.decorators import api_view, renderer_classes  from rest_framework.renderers import TemplateHTMLRenderer  from rest_framework.response import Response -from rest_framework.compat import six +import django.template.loader  @api_view(('GET',)) @@ -34,7 +34,8 @@ def not_found(request):      raise Http404() -urlpatterns = patterns('', +urlpatterns = patterns( +    '',      url(r'^$', example),      url(r'^permission_denied$', permission_denied),      url(r'^not_found$', not_found), @@ -42,7 +43,7 @@ urlpatterns = patterns('',  class TemplateHTMLRendererTests(TestCase): -    urls = 'rest_framework.tests.test_htmlrenderer' +    urls = 'tests.test_htmlrenderer'      def setUp(self):          """ @@ -82,7 +83,7 @@ class TemplateHTMLRendererTests(TestCase):  class TemplateHTMLRendererExceptionTests(TestCase): -    urls = 'rest_framework.tests.test_htmlrenderer' +    urls = 'tests.test_htmlrenderer'      def setUp(self):          """ diff --git a/rest_framework/tests/test_hyperlinkedserializers.py b/tests/test_hyperlinkedserializers.py index 83d46043..d4548539 100644 --- a/rest_framework/tests/test_hyperlinkedserializers.py +++ b/tests/test_hyperlinkedserializers.py @@ -2,10 +2,10 @@ from __future__ import unicode_literals  import json  from django.test import TestCase  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.test import APIRequestFactory -from rest_framework.tests.models import ( +from tests.models import (      Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment,      Album, Photo, OptionalRelationModel  ) @@ -25,7 +25,7 @@ class BlogPostCommentSerializer(serializers.ModelSerializer):  class PhotoSerializer(serializers.Serializer):      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):          return Photo(**attrs) @@ -94,7 +94,8 @@ class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):      model_serializer_class = serializers.HyperlinkedModelSerializer -urlpatterns = patterns('', +urlpatterns = patterns( +    '',      url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),      url(r'^basic/(?P<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'),      url(r'^anchor/(?P<pk>\d+)/$', AnchorDetail.as_view(), name='anchor-detail'), @@ -110,7 +111,7 @@ urlpatterns = patterns('',  class TestBasicHyperlinkedView(TestCase): -    urls = 'rest_framework.tests.test_hyperlinkedserializers' +    urls = 'tests.test_hyperlinkedserializers'      def setUp(self):          """ @@ -147,7 +148,7 @@ class TestBasicHyperlinkedView(TestCase):  class TestManyToManyHyperlinkedView(TestCase): -    urls = 'rest_framework.tests.test_hyperlinkedserializers' +    urls = 'tests.test_hyperlinkedserializers'      def setUp(self):          """ @@ -195,7 +196,7 @@ class TestManyToManyHyperlinkedView(TestCase):  class TestHyperlinkedIdentityFieldLookup(TestCase): -    urls = 'rest_framework.tests.test_hyperlinkedserializers' +    urls = 'tests.test_hyperlinkedserializers'      def setUp(self):          """ @@ -225,7 +226,7 @@ class TestHyperlinkedIdentityFieldLookup(TestCase):  class TestCreateWithForeignKeys(TestCase): -    urls = 'rest_framework.tests.test_hyperlinkedserializers' +    urls = 'tests.test_hyperlinkedserializers'      def setUp(self):          """ @@ -250,7 +251,7 @@ class TestCreateWithForeignKeys(TestCase):  class TestCreateWithForeignKeysAndCustomSlug(TestCase): -    urls = 'rest_framework.tests.test_hyperlinkedserializers' +    urls = 'tests.test_hyperlinkedserializers'      def setUp(self):          """ @@ -275,7 +276,7 @@ class TestCreateWithForeignKeysAndCustomSlug(TestCase):  class TestOptionalRelationHyperlinkedView(TestCase): -    urls = 'rest_framework.tests.test_hyperlinkedserializers' +    urls = 'tests.test_hyperlinkedserializers'      def setUp(self):          """ @@ -335,7 +336,7 @@ class TestOverriddenURLField(TestCase):  class TestURLFieldNameBySettings(TestCase): -    urls = 'rest_framework.tests.test_hyperlinkedserializers' +    urls = 'tests.test_hyperlinkedserializers'      def setUp(self):          self.saved_url_field_name = api_settings.URL_FIELD_NAME @@ -360,7 +361,7 @@ class TestURLFieldNameBySettings(TestCase):  class TestURLFieldNameByOptions(TestCase): -    urls = 'rest_framework.tests.test_hyperlinkedserializers' +    urls = 'tests.test_hyperlinkedserializers'      def setUp(self):          class Serializer(serializers.HyperlinkedModelSerializer): diff --git a/rest_framework/tests/test_multitable_inheritance.py b/tests/test_multitable_inheritance.py index 00c15327..ce1bf3ea 100644 --- a/rest_framework/tests/test_multitable_inheritance.py +++ b/tests/test_multitable_inheritance.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals  from django.db import models  from django.test import TestCase  from rest_framework import serializers -from rest_framework.tests.models import RESTFrameworkModel +from tests.models import RESTFrameworkModel  # Models diff --git a/rest_framework/tests/test_negotiation.py b/tests/test_negotiation.py index 04b89eb6..04b89eb6 100644 --- a/rest_framework/tests/test_negotiation.py +++ b/tests/test_negotiation.py diff --git a/rest_framework/tests/test_nullable_fields.py b/tests/test_nullable_fields.py index 6ee55c00..0c133fc2 100644 --- a/rest_framework/tests/test_nullable_fields.py +++ b/tests/test_nullable_fields.py @@ -1,10 +1,10 @@  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.tests.models import NullableForeignKeySource -from rest_framework.tests.serializers import NullableFKSourceSerializer -from rest_framework.tests.views import NullableFKSourceDetail +from tests.models import NullableForeignKeySource +from tests.serializers import NullableFKSourceSerializer +from tests.views import NullableFKSourceDetail  urlpatterns = patterns( @@ -18,7 +18,7 @@ class NullableForeignKeyTests(APITestCase):      DRF should be able to handle nullable foreign keys when a test      Client POST/PUT request is made with its own serialized object.      """ -    urls = 'rest_framework.tests.test_nullable_fields' +    urls = 'tests.test_nullable_fields'      def test_updating_object_with_null_fk(self):          obj = NullableForeignKeySource(name='example', target=None) diff --git a/rest_framework/tests/test_pagination.py b/tests/test_pagination.py index 24c1ba39..80c33e2e 100644 --- a/rest_framework/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -1,18 +1,17 @@  from __future__ import unicode_literals  import datetime  from decimal import Decimal -from django.db import models  from django.core.paginator import Paginator  from django.test import TestCase  from django.utils import unittest  from rest_framework import generics, status, pagination, filters, serializers  from rest_framework.compat import django_filters  from rest_framework.test import APIRequestFactory -from rest_framework.tests.models import BasicModel -from .models import FilterableItem +from .models import BasicModel, FilterableItem  factory = APIRequestFactory() +  # Helper function to split arguments out of an url  def split_arguments_from_url(url):      if '?' not in url: @@ -275,8 +274,8 @@ class TestUnpaginated(TestCase):              BasicModel(text=i).save()          self.objects = BasicModel.objects          self.data = [ -        {'id': obj.id, 'text': obj.text} -        for obj in self.objects.all() +            {'id': obj.id, 'text': obj.text} +            for obj in self.objects.all()          ]          self.view = DefaultPageSizeKwargView.as_view() @@ -303,8 +302,8 @@ class TestCustomPaginateByParam(TestCase):              BasicModel(text=i).save()          self.objects = BasicModel.objects          self.data = [ -        {'id': obj.id, 'text': obj.text} -        for obj in self.objects.all() +            {'id': obj.id, 'text': obj.text} +            for obj in self.objects.all()          ]          self.view = PaginateByParamView.as_view() @@ -364,11 +363,11 @@ class TestMaxPaginateByParam(TestCase):          self.assertEqual(response.data['results'], self.data[:3]) -### Tests for context in pagination serializers +# Tests for context in pagination serializers  class CustomField(serializers.Field):      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")          return "value" @@ -378,7 +377,7 @@ class BasicModelSerializer(serializers.Serializer):      def __init__(self, *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") @@ -399,7 +398,7 @@ class TestContextPassedToCustomField(TestCase):          self.assertEqual(response.status_code, status.HTTP_200_OK) -### Tests for custom pagination serializers +# Tests for custom pagination serializers  class LinksSerializer(serializers.Serializer):      next = pagination.NextPageField(source='*') @@ -484,8 +483,6 @@ class NonIntegerPaginator(object):  class TestNonIntegerPagination(TestCase): - -      def test_custom_pagination_serializer(self):          objects = ['john', 'paul', 'george', 'ringo']          paginator = NonIntegerPaginator(objects, 2) diff --git a/rest_framework/tests/test_parsers.py b/tests/test_parsers.py index 8af90677..8af90677 100644 --- a/rest_framework/tests/test_parsers.py +++ b/tests/test_parsers.py diff --git a/rest_framework/tests/test_permissions.py b/tests/test_permissions.py index 6e3a6303..93f8020f 100644 --- a/rest_framework/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -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.filters import DjangoObjectPermissionsFilter  from rest_framework.test import APIRequestFactory -from rest_framework.tests.models import BasicModel +from tests.models import BasicModel  import base64  factory = APIRequestFactory() +  class RootView(generics.ListCreateAPIView):      model = BasicModel      authentication_classes = [authentication.BasicAuthentication] @@ -101,42 +102,54 @@ class ModelPermissionsIntegrationTests(TestCase):          self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)      def test_options_permitted(self): -        request = factory.options('/', -                               HTTP_AUTHORIZATION=self.permitted_credentials) +        request = factory.options( +            '/', +            HTTP_AUTHORIZATION=self.permitted_credentials +        )          response = root_view(request, pk='1')          self.assertEqual(response.status_code, status.HTTP_200_OK)          self.assertIn('actions', response.data)          self.assertEqual(list(response.data['actions'].keys()), ['POST']) -        request = factory.options('/1', -                               HTTP_AUTHORIZATION=self.permitted_credentials) +        request = factory.options( +            '/1', +            HTTP_AUTHORIZATION=self.permitted_credentials +        )          response = instance_view(request, pk='1')          self.assertEqual(response.status_code, status.HTTP_200_OK)          self.assertIn('actions', response.data)          self.assertEqual(list(response.data['actions'].keys()), ['PUT'])      def test_options_disallowed(self): -        request = factory.options('/', -                               HTTP_AUTHORIZATION=self.disallowed_credentials) +        request = factory.options( +            '/', +            HTTP_AUTHORIZATION=self.disallowed_credentials +        )          response = root_view(request, pk='1')          self.assertEqual(response.status_code, status.HTTP_200_OK)          self.assertNotIn('actions', response.data) -        request = factory.options('/1', -                               HTTP_AUTHORIZATION=self.disallowed_credentials) +        request = factory.options( +            '/1', +            HTTP_AUTHORIZATION=self.disallowed_credentials +        )          response = instance_view(request, pk='1')          self.assertEqual(response.status_code, status.HTTP_200_OK)          self.assertNotIn('actions', response.data)      def test_options_updateonly(self): -        request = factory.options('/', -                               HTTP_AUTHORIZATION=self.updateonly_credentials) +        request = factory.options( +            '/', +            HTTP_AUTHORIZATION=self.updateonly_credentials +        )          response = root_view(request, pk='1')          self.assertEqual(response.status_code, status.HTTP_200_OK)          self.assertNotIn('actions', response.data) -        request = factory.options('/1', -                               HTTP_AUTHORIZATION=self.updateonly_credentials) +        request = factory.options( +            '/1', +            HTTP_AUTHORIZATION=self.updateonly_credentials +        )          response = instance_view(request, pk='1')          self.assertEqual(response.status_code, status.HTTP_200_OK)          self.assertIn('actions', response.data) @@ -153,6 +166,7 @@ class BasicPermModel(models.Model):              # add, change, delete built in to django          ) +  # Custom object-level permission, that includes 'view' permissions  class ViewObjectPermissions(permissions.DjangoObjectPermissions):      perms_map = { @@ -187,8 +201,7 @@ class ObjectPermissionsIntegrationTests(TestCase):      """      Integration tests for the object level permissions API.      """ -    @classmethod -    def setUpClass(cls): +    def setUp(self):          from guardian.shortcuts import assign_perm          # create users @@ -206,7 +219,7 @@ class ObjectPermissionsIntegrationTests(TestCase):          app_label = BasicPermModel._meta.app_label          f = '{0}_{1}'.format          perms = { -            'view':   f('view', model_name), +            'view': f('view', model_name),              'change': f('change', model_name),              'delete': f('delete', model_name)          } @@ -215,21 +228,13 @@ class ObjectPermissionsIntegrationTests(TestCase):              assign_perm(perm, everyone)          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          readers = Group.objects.create(name='readers')          writers = Group.objects.create(name='writers')          deleters = Group.objects.create(name='deleters')          model = BasicPermModel.objects.create(text='foo') -         +          assign_perm(perms['view'], readers, model)          assign_perm(perms['change'], writers, model)          assign_perm(perms['delete'], deleters, model) @@ -255,21 +260,27 @@ class ObjectPermissionsIntegrationTests(TestCase):      # Update      def test_can_update_permissions(self): -        request = factory.patch('/1', {'text': 'foobar'}, format='json', -            HTTP_AUTHORIZATION=self.credentials['writeonly']) +        request = factory.patch( +            '/1', {'text': 'foobar'}, format='json', +            HTTP_AUTHORIZATION=self.credentials['writeonly'] +        )          response = object_permissions_view(request, pk='1')          self.assertEqual(response.status_code, status.HTTP_200_OK)          self.assertEqual(response.data.get('text'), 'foobar')      def test_cannot_update_permissions(self): -        request = factory.patch('/1', {'text': 'foobar'}, format='json', -            HTTP_AUTHORIZATION=self.credentials['deleteonly']) +        request = factory.patch( +            '/1', {'text': 'foobar'}, format='json', +            HTTP_AUTHORIZATION=self.credentials['deleteonly'] +        )          response = object_permissions_view(request, pk='1')          self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)      def test_cannot_update_permissions_non_existing(self): -        request = factory.patch('/999', {'text': 'foobar'}, format='json', -            HTTP_AUTHORIZATION=self.credentials['deleteonly']) +        request = factory.patch( +            '/999', {'text': 'foobar'}, format='json', +            HTTP_AUTHORIZATION=self.credentials['deleteonly'] +        )          response = object_permissions_view(request, pk='999')          self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/rest_framework/tests/test_relations.py b/tests/test_relations.py index 37ac826b..bc1db69f 100644 --- a/rest_framework/tests/test_relations.py +++ b/tests/test_relations.py @@ -7,7 +7,7 @@ from django.db import models  from django.test import TestCase  from django.utils import unittest  from rest_framework import serializers -from rest_framework.tests.models import BlogPost +from tests.models import BlogPost  class NullModel(models.Model): @@ -107,20 +107,26 @@ class RelatedFieldSourceTests(TestCase):          Check that the exception message are correct if the source field          doesn't exist.          """ -        from rest_framework.tests.models import ManyToManySource +        from tests.models import ManyToManySource +          class Meta:              model = ManyToManySource +          attrs = {              'name': serializers.SlugRelatedField(                  slug_field='name', source='banzai'),              'Meta': Meta,          } -        TestSerializer = type(str('TestSerializer'), -            (serializers.ModelSerializer,), attrs) +        TestSerializer = type( +            str('TestSerializer'), +            (serializers.ModelSerializer,), +            attrs +        )          with self.assertRaises(AttributeError):              TestSerializer(data={'name': 'foo'}) +  @unittest.skipIf(get_version() < '1.6.0', 'Upstream behaviour changed in v1.6')  class RelatedFieldChoicesTests(TestCase):      """ @@ -141,4 +147,3 @@ class RelatedFieldChoicesTests(TestCase):          widget_count = len(field.widget.choices)          self.assertEqual(widget_count, choice_count + 1, 'BLANK_CHOICE_DASH option should have been added') - diff --git a/rest_framework/tests/test_relations_hyperlink.py b/tests/test_relations_hyperlink.py index 3c4d39af..0c8eb254 100644 --- a/rest_framework/tests/test_relations_hyperlink.py +++ b/tests/test_relations_hyperlink.py @@ -1,9 +1,9 @@  from __future__ import unicode_literals +from django.conf.urls import patterns, url  from django.test import TestCase  from rest_framework import serializers -from rest_framework.compat import patterns, url  from rest_framework.test import APIRequestFactory -from rest_framework.tests.models import ( +from tests.models import (      BlogPost,      ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,      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):      pass -urlpatterns = patterns('', +urlpatterns = patterns( +    '',      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'^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  class HyperlinkedManyToManyTests(TestCase): -    urls = 'rest_framework.tests.test_relations_hyperlink' +    urls = 'tests.test_relations_hyperlink'      def setUp(self):          for idx in range(1, 4): @@ -86,9 +87,9 @@ class HyperlinkedManyToManyTests(TestCase):          queryset = ManyToManySource.objects.all()          serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})          expected = [ -                {'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/3/', 'name': 'source-3', '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/']}, +            {'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/']}          ]          self.assertEqual(serializer.data, expected) @@ -114,9 +115,9 @@ class HyperlinkedManyToManyTests(TestCase):          queryset = ManyToManySource.objects.all()          serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})          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/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/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/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}          ]          self.assertEqual(serializer.data, expected) @@ -179,7 +180,7 @@ class HyperlinkedManyToManyTests(TestCase):  class HyperlinkedForeignKeyTests(TestCase): -    urls = 'rest_framework.tests.test_relations_hyperlink' +    urls = 'tests.test_relations_hyperlink'      def setUp(self):          target = ForeignKeyTarget(name='target-1') @@ -307,7 +308,7 @@ class HyperlinkedForeignKeyTests(TestCase):  class HyperlinkedNullableForeignKeyTests(TestCase): -    urls = 'rest_framework.tests.test_relations_hyperlink' +    urls = 'tests.test_relations_hyperlink'      def setUp(self):          target = ForeignKeyTarget(name='target-1') @@ -435,7 +436,7 @@ class HyperlinkedNullableForeignKeyTests(TestCase):  class HyperlinkedNullableOneToOneTests(TestCase): -    urls = 'rest_framework.tests.test_relations_hyperlink' +    urls = 'tests.test_relations_hyperlink'      def setUp(self):          target = OneToOneTarget(name='target-1') @@ -458,7 +459,7 @@ class HyperlinkedNullableOneToOneTests(TestCase):  # Regression tests for #694 (`source` attribute on related fields)  class HyperlinkedRelatedFieldSourceTests(TestCase): -    urls = 'rest_framework.tests.test_relations_hyperlink' +    urls = 'tests.test_relations_hyperlink'      def test_related_manager_source(self):          """ diff --git a/rest_framework/tests/test_relations_nested.py b/tests/test_relations_nested.py index 4d9da489..4d9da489 100644 --- a/rest_framework/tests/test_relations_nested.py +++ b/tests/test_relations_nested.py diff --git a/rest_framework/tests/test_relations_pk.py b/tests/test_relations_pk.py index 3815afdd..e3f836ed 100644 --- a/rest_framework/tests/test_relations_pk.py +++ b/tests/test_relations_pk.py @@ -1,12 +1,12 @@  from __future__ import unicode_literals  from django.db import models  from django.test import TestCase +from django.utils import six  from rest_framework import serializers -from rest_framework.tests.models import ( +from tests.models import (      BlogPost, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,      NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource,  ) -from rest_framework.compat import six  # ManyToMany @@ -65,9 +65,9 @@ class PKManyToManyTests(TestCase):          queryset = ManyToManySource.objects.all()          serializer = ManyToManySourceSerializer(queryset, many=True)          expected = [ -                {'id': 1, 'name': 'source-1', 'targets': [1]}, -                {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, -                {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]} +            {'id': 1, 'name': 'source-1', 'targets': [1]}, +            {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, +            {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}          ]          self.assertEqual(serializer.data, expected) @@ -93,9 +93,9 @@ class PKManyToManyTests(TestCase):          queryset = ManyToManySource.objects.all()          serializer = ManyToManySourceSerializer(queryset, many=True)          expected = [ -                {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]}, -                {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, -                {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]} +            {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]}, +            {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, +            {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}          ]          self.assertEqual(serializer.data, expected) diff --git a/rest_framework/tests/test_relations_slug.py b/tests/test_relations_slug.py index 435c821c..97ebf23a 100644 --- a/rest_framework/tests/test_relations_slug.py +++ b/tests/test_relations_slug.py @@ -1,6 +1,6 @@  from django.test import TestCase  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): diff --git a/rest_framework/tests/test_renderers.py b/tests/test_renderers.py index 7cb7d0f9..91244e26 100644 --- a/rest_framework/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -2,13 +2,14 @@  from __future__ import unicode_literals  from decimal import Decimal +from django.conf.urls import patterns, url, include  from django.core.cache import cache  from django.db import models  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 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.views import APIView  from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ @@ -75,7 +76,6 @@ class MockGETView(APIView):          return Response({'foo': ['bar', 'baz']}) -  class MockPOSTView(APIView):      def post(self, request, **kwargs):          return Response({'foo': request.DATA}) @@ -101,7 +101,8 @@ class HTMLView1(APIView):      def get(self, request, **kwargs):          return Response('text') -urlpatterns = patterns('', +urlpatterns = patterns( +    '',      url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),      url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),      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.      """ -    urls = 'rest_framework.tests.test_renderers' +    urls = 'tests.test_renderers'      def test_default_renderer_serializes_content(self):          """If the Accept header is not set the default renderer should serialize the response.""" @@ -311,16 +312,22 @@ class JSONRendererTests(TestCase):          class Dict(MutableMapping):              def __init__(self):                  self._dict = dict() +              def __getitem__(self, key):                  return self._dict.__getitem__(key) +              def __setitem__(self, key, value):                  return self._dict.__setitem__(key, value) +              def __delitem__(self, key):                  return self._dict.__delitem__(key) +              def __iter__(self):                  return self._dict.__iter__() +              def __len__(self):                  return self._dict.__len__() +              def keys(self):                  return self._dict.keys() @@ -329,22 +336,24 @@ class JSONRendererTests(TestCase):          x[2] = 3          ret = JSONRenderer().render(x)          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):          class DictLike(object):              def __init__(self):                  self._dict = {} +              def set(self, value):                  self._dict = dict(value) +              def __getitem__(self, key):                  return self._dict[key] -             +          x = DictLike()          x.set({'a': 1, 'b': 'string'})          with self.assertRaises(TypeError):              JSONRenderer().render(x) -         +      def test_without_content_type_args(self):          """          Test basic JSON rendering. @@ -387,41 +396,53 @@ class JSONPRendererTests(TestCase):      Tests specific to the JSONP Renderer      """ -    urls = 'rest_framework.tests.test_renderers' +    urls = 'tests.test_renderers'      def test_without_callback_with_json_renderer(self):          """          Test JSONP rendering with View JSON Renderer.          """ -        resp = self.client.get('/jsonp/jsonrenderer', -                               HTTP_ACCEPT='application/javascript') +        resp = self.client.get( +            '/jsonp/jsonrenderer', +            HTTP_ACCEPT='application/javascript' +        )          self.assertEqual(resp.status_code, status.HTTP_200_OK)          self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8') -        self.assertEqual(resp.content, -            ('callback(%s);' % _flat_repr).encode('ascii')) +        self.assertEqual( +            resp.content, +            ('callback(%s);' % _flat_repr).encode('ascii') +        )      def test_without_callback_without_json_renderer(self):          """          Test JSONP rendering without View JSON Renderer.          """ -        resp = self.client.get('/jsonp/nojsonrenderer', -                               HTTP_ACCEPT='application/javascript') +        resp = self.client.get( +            '/jsonp/nojsonrenderer', +            HTTP_ACCEPT='application/javascript' +        )          self.assertEqual(resp.status_code, status.HTTP_200_OK)          self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8') -        self.assertEqual(resp.content, -            ('callback(%s);' % _flat_repr).encode('ascii')) +        self.assertEqual( +            resp.content, +            ('callback(%s);' % _flat_repr).encode('ascii') +        )      def test_with_callback(self):          """          Test JSONP rendering with callback function name.          """          callback_func = 'myjsonpcallback' -        resp = self.client.get('/jsonp/nojsonrenderer?callback=' + callback_func, -                               HTTP_ACCEPT='application/javascript') +        resp = self.client.get( +            '/jsonp/nojsonrenderer?callback=' + callback_func, +            HTTP_ACCEPT='application/javascript' +        )          self.assertEqual(resp.status_code, status.HTTP_200_OK)          self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8') -        self.assertEqual(resp.content, -            ('%s(%s);' % (callback_func, _flat_repr)).encode('ascii')) +        self.assertEqual( +            resp.content, +            ('%s(%s);' % (callback_func, _flat_repr)).encode('ascii') +        )  if yaml: @@ -466,7 +487,6 @@ if yaml:          def assertYAMLContains(self, content, string):              self.assertTrue(string in content, '%r not in %r' % (string, content)) -      class UnicodeYAMLRendererTests(TestCase):          """          Tests specific for the Unicode YAML Renderer @@ -582,7 +602,7 @@ class CacheRenderTest(TestCase):      Tests specific to caching responses      """ -    urls = 'rest_framework.tests.test_renderers' +    urls = 'tests.test_renderers'      cache_key = 'just_a_cache_key' @@ -591,13 +611,13 @@ class CacheRenderTest(TestCase):          """ Return any errors that would be raised if `obj' is pickled          Courtesy of koffie @ http://stackoverflow.com/a/7218986/109897          """ -        if seen == None: +        if seen is None:              seen = []          try:              state = obj.__getstate__()          except AttributeError:              return -        if state == None: +        if state is None:              return          if isinstance(state, tuple):              if not isinstance(state[0], dict): diff --git a/rest_framework/tests/test_request.py b/tests/test_request.py index c0b50f33..8ddaf0a7 100644 --- a/rest_framework/tests/test_request.py +++ b/tests/test_request.py @@ -2,14 +2,15 @@  Tests for content parsing, and form-overloaded content parsing.  """  from __future__ import unicode_literals +from django.conf.urls import patterns  from django.contrib.auth.models import User  from django.contrib.auth import authenticate, login, logout  from django.contrib.sessions.middleware import SessionMiddleware  from django.core.handlers.wsgi import WSGIRequest  from django.test import TestCase +from django.utils import six  from rest_framework import status  from rest_framework.authentication import SessionAuthentication -from rest_framework.compat import patterns  from rest_framework.parsers import (      BaseParser,      FormParser, @@ -21,7 +22,6 @@ from rest_framework.response import Response  from rest_framework.settings import api_settings  from rest_framework.test import APIRequestFactory, APIClient  from rest_framework.views import APIView -from rest_framework.compat import six  from io import BytesIO  import json @@ -272,13 +272,14 @@ class MockView(APIView):          return Response(status=status.INTERNAL_SERVER_ERROR) -urlpatterns = patterns('', +urlpatterns = patterns( +    '',      (r'^$', MockView.as_view()),  )  class TestContentParsingWithAuthentication(TestCase): -    urls = 'rest_framework.tests.test_request' +    urls = 'tests.test_request'      def setUp(self):          self.csrf_client = APIClient(enforce_csrf_checks=True) diff --git a/rest_framework/tests/test_response.py b/tests/test_response.py index eea3c641..2eff83d3 100644 --- a/rest_framework/tests/test_response.py +++ b/tests/test_response.py @@ -1,7 +1,8 @@  from __future__ import unicode_literals +from django.conf.urls import patterns, url, include  from django.test import TestCase -from rest_framework.tests.models import BasicModel, BasicModelSerializer -from rest_framework.compat import patterns, url, include +from django.utils import six +from tests.models import BasicModel, BasicModelSerializer  from rest_framework.response import Response  from rest_framework.views import APIView  from rest_framework import generics @@ -14,7 +15,6 @@ from rest_framework.renderers import (  )  from rest_framework import viewsets  from rest_framework.settings import api_settings -from rest_framework.compat import six  class MockPickleRenderer(BaseRenderer): @@ -100,7 +100,8 @@ new_model_viewset_router = routers.DefaultRouter()  new_model_viewset_router.register(r'', HTMLNewModelViewSet) -urlpatterns = patterns('', +urlpatterns = patterns( +    '',      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'^$', 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.      """ -    urls = 'rest_framework.tests.test_response' +    urls = 'tests.test_response'      def test_default_renderer_serializes_content(self):          """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.      """ -    urls = 'rest_framework.tests.test_response' +    urls = 'tests.test_response'      def test_only_html_renderer(self):          """ @@ -218,7 +219,7 @@ class Issue467Tests(TestCase):      Tests for #467      """ -    urls = 'rest_framework.tests.test_response' +    urls = 'tests.test_response'      def test_form_has_label_and_help_text(self):          resp = self.client.get('/html_new_model') @@ -232,7 +233,7 @@ class Issue807Tests(TestCase):      Covers #807      """ -    urls = 'rest_framework.tests.test_response' +    urls = 'tests.test_response'      def test_does_not_append_charset_by_default(self):          """ diff --git a/rest_framework/tests/test_reverse.py b/tests/test_reverse.py index 690a30b1..675a9d5a 100644 --- a/rest_framework/tests/test_reverse.py +++ b/tests/test_reverse.py @@ -1,6 +1,6 @@  from __future__ import unicode_literals +from django.conf.urls import patterns, url  from django.test import TestCase -from rest_framework.compat import patterns, url  from rest_framework.reverse import reverse  from rest_framework.test import APIRequestFactory @@ -10,7 +10,8 @@ factory = APIRequestFactory()  def null_view(request):      pass -urlpatterns = patterns('', +urlpatterns = patterns( +    '',      url(r'^view$', null_view, name='view'),  ) @@ -19,7 +20,7 @@ class ReverseTests(TestCase):      """      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):          request = factory.get('/view') diff --git a/rest_framework/tests/test_routers.py b/tests/test_routers.py index e723f7d4..b076f134 100644 --- a/rest_framework/tests/test_routers.py +++ b/tests/test_routers.py @@ -1,10 +1,10 @@  from __future__ import unicode_literals +from django.conf.urls import patterns, url, include  from django.db import models  from django.test import TestCase  from django.core.exceptions import ImproperlyConfigured  from rest_framework import serializers, viewsets, permissions -from rest_framework.compat import include, patterns, url -from rest_framework.decorators import link, action +from rest_framework.decorators import detail_route, list_route  from rest_framework.response import Response  from rest_framework.routers import SimpleRouter, DefaultRouter  from rest_framework.test import APIRequestFactory @@ -18,23 +18,23 @@ class BasicViewSet(viewsets.ViewSet):      def list(self, request, *args, **kwargs):          return Response({'method': 'list'}) -    @action() +    @detail_route(methods=['post'])      def action1(self, request, *args, **kwargs):          return Response({'method': 'action1'}) -    @action() +    @detail_route(methods=['post'])      def action2(self, request, *args, **kwargs):          return Response({'method': 'action2'}) -    @action(methods=['post', 'delete']) +    @detail_route(methods=['post', 'delete'])      def action3(self, request, *args, **kwargs):          return Response({'method': 'action2'}) -    @link() +    @detail_route()      def link1(self, request, *args, **kwargs):          return Response({'method': 'link1'}) -    @link() +    @detail_route()      def link2(self, request, *args, **kwargs):          return Response({'method': 'link2'}) @@ -72,7 +72,7 @@ class TestCustomLookupFields(TestCase):      """      Ensure that custom lookup fields are correctly routed.      """ -    urls = 'rest_framework.tests.test_routers' +    urls = 'tests.test_routers'      def setUp(self):          class NoteSerializer(serializers.HyperlinkedModelSerializer): @@ -91,9 +91,10 @@ class TestCustomLookupFields(TestCase):          self.router = SimpleRouter()          self.router.register(r'notes', NoteViewSet) -        from rest_framework.tests import test_routers +        from tests import test_routers          urls = getattr(test_routers, 'urlpatterns') -        urls += patterns('', +        urls += patterns( +            '',              url(r'^', include(self.router.urls)),          ) @@ -104,7 +105,8 @@ class TestCustomLookupFields(TestCase):      def test_retrieve_lookup_field_list_view(self):          response = self.client.get('/notes/') -        self.assertEqual(response.data, +        self.assertEqual( +            response.data,              [{                  "url": "http://testserver/notes/123/",                  "uuid": "123", "text": "foo bar" @@ -113,7 +115,8 @@ class TestCustomLookupFields(TestCase):      def test_retrieve_lookup_field_detail_view(self):          response = self.client.get('/notes/123/') -        self.assertEqual(response.data, +        self.assertEqual( +            response.data,              {                  "url": "http://testserver/notes/123/",                  "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):      def setUp(self):          class NoteViewSet(viewsets.ModelViewSet): @@ -131,7 +155,7 @@ class TestTrailingSlashIncluded(TestCase):          self.urls = self.router.urls      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)):              self.assertEqual(expected[idx], self.urls[idx].regex.pattern) @@ -175,7 +199,7 @@ class TestActionKeywordArgs(TestCase):          class TestViewSet(viewsets.ModelViewSet):              permission_classes = [] -            @action(permission_classes=[permissions.AllowAny]) +            @detail_route(methods=['post'], permission_classes=[permissions.AllowAny])              def custom(self, request, *args, **kwargs):                  return Response({                      'permission_classes': self.permission_classes @@ -196,14 +220,14 @@ class TestActionKeywordArgs(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      """      def test_exception_raised_when_action_applied_to_existing_route(self):          class TestViewSet(viewsets.ModelViewSet): -            @action() +            @detail_route(methods=['post'])              def retrieve(self, request, *args, **kwargs):                  return Response({                      'hello': 'world' @@ -214,3 +238,49 @@ class TestActionAppliedToExistingRoute(TestCase):          with self.assertRaises(ImproperlyConfigured):              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) diff --git a/rest_framework/tests/test_serializer.py b/tests/test_serializer.py index fb2eac0b..90f37cf2 100644 --- a/rest_framework/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -7,11 +7,13 @@ from django.utils import unittest  from django.utils.datastructures import MultiValueDict  from django.utils.translation import ugettext_lazy as _  from rest_framework import serializers, fields, relations -from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, -    BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel, -    ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel, -    ForeignKeySource, ManyToManySource) -from rest_framework.tests.models import BasicModelSerializer +from tests.models import ( +    HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, +    BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, +    DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, +    RESTFrameworkModel, ForeignKeySource +) +from tests.models import BasicModelSerializer  import datetime  import pickle  try: @@ -30,6 +32,7 @@ if PIL is not None:          image_field = models.ImageField(upload_to='test', max_length=1024, blank=True)          slug_field = models.SlugField(max_length=1024, blank=True)          url_field = models.URLField(max_length=1024, blank=True) +        nullable_char_field = models.CharField(max_length=1024, blank=True, null=True)      class DVOAFModel(RESTFrameworkModel):          positive_integer_field = models.PositiveIntegerField(blank=True) @@ -98,6 +101,7 @@ class ActionItemSerializer(serializers.ModelSerializer):      class Meta:          model = ActionItem +  class ActionItemSerializerOptionalFields(serializers.ModelSerializer):      """      Intended to test that fields with `required=False` are excluded from validation. @@ -108,6 +112,7 @@ class ActionItemSerializerOptionalFields(serializers.ModelSerializer):          model = ActionItem          fields = ('title',) +  class ActionItemSerializerCustomRestore(serializers.ModelSerializer):      class Meta: @@ -294,8 +299,10 @@ class BasicTests(TestCase):          in the Meta data          """          serializer = PersonSerializer(self.person) -        self.assertEqual(set(serializer.data.keys()), -                          set(['name', 'age', 'info'])) +        self.assertEqual( +            set(serializer.data.keys()), +            set(['name', 'age', 'info']) +        )      def test_field_with_dictionary(self):          """ @@ -330,9 +337,9 @@ class BasicTests(TestCase):              — id field is not populated if `data` is accessed prior to `save()`          """          serializer = ActionItemSerializer(self.actionitem) -        self.assertIsNone(serializer.data.get('id',None), 'New instance. `id` should not be set.') +        self.assertIsNone(serializer.data.get('id', None), 'New instance. `id` should not be set.')          serializer.save() -        self.assertIsNotNone(serializer.data.get('id',None), 'Model is saved. `id` should be set.') +        self.assertIsNotNone(serializer.data.get('id', None), 'Model is saved. `id` should be set.')      def test_fields_marked_as_not_required_are_excluded_from_validation(self):          """ @@ -408,7 +415,7 @@ class ValidationTests(TestCase):          mistaken for not having a default."""          data = {              'title': 'Some action item', -            #No 'done' value. +            # No 'done' value.          }          serializer = ActionItemSerializer(self.actionitem, data=data)          self.assertEqual(serializer.is_valid(), True) @@ -659,10 +666,10 @@ class ModelValidationTests(TestCase):          serializer.save()          second_serializer = AlbumsSerializer(data={'title': 'a'})          self.assertFalse(second_serializer.is_valid()) -        self.assertEqual(second_serializer.errors,  {'title': ['Album with this Title already exists.'],}) -        third_serializer = AlbumsSerializer(data=[{'title': 'b', 'ref': '1'}, {'title': 'c'}]) +        self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']}) +        third_serializer = AlbumsSerializer(data=[{'title': 'b', 'ref': '1'}, {'title': 'c'}], many=True)          self.assertFalse(third_serializer.is_valid()) -        self.assertEqual(third_serializer.errors,  [{'ref': ['Album with this Ref already exists.']}, {}]) +        self.assertEqual(third_serializer.errors, [{'ref': ['Album with this Ref already exists.']}, {}])      def test_foreign_key_is_null_with_partial(self):          """ @@ -958,7 +965,7 @@ class WritableFieldDefaultValueTests(TestCase):          self.assertEqual(got, self.expected)      def test_get_default_value_with_callable(self): -        field = self.create_field(default=lambda : self.expected) +        field = self.create_field(default=lambda: self.expected)          got = field.get_default_value()          self.assertEqual(got, self.expected) @@ -973,7 +980,7 @@ class WritableFieldDefaultValueTests(TestCase):          self.assertIsNone(got)      def test_get_default_value_returns_non_True_values(self): -        values = [None, '', False, 0, [], (), {}] # values that assumed as 'False' in the 'if' clause +        values = [None, '', False, 0, [], (), {}]  # values that assumed as 'False' in the 'if' clause          for expected in values:              field = self.create_field(default=expected)              got = field.get_default_value() @@ -1152,7 +1159,7 @@ class RelatedTraversalTest(TestCase):          """          If a component of the dotted.source is None, return None for the field.          """ -        from rest_framework.tests.models import NullableForeignKeySource +        from tests.models import NullableForeignKeySource          instance = NullableForeignKeySource.objects.create(name='Source with null FK')          class NullableSourceSerializer(serializers.Serializer): @@ -1260,8 +1267,22 @@ class BlankFieldTests(TestCase):          serializer = self.model_serializer_class(data={})          self.assertEqual(serializer.is_valid(), True) +    def test_create_model_null_field_save(self): +        """ +        Regression test for #1330. -#test for issue #460 +        https://github.com/tomchristie/django-rest-framework/pull/1330 +        """ +        serializer = self.model_serializer_class(data={'title': None}) +        self.assertEqual(serializer.is_valid(), True) + +        try: +            serializer.save() +        except Exception: +            self.fail('Exception raised on save() after validation passes') + + +# Test for issue #460  class SerializerPickleTests(TestCase):      """      Test pickleability of the output of Serializers @@ -1485,7 +1506,7 @@ class NestedSerializerContextTests(TestCase):              callable = serializers.SerializerMethodField('_callable')              def _callable(self, instance): -                if not 'context_item' in self.context: +                if 'context_item' not in self.context:                      raise RuntimeError("context isn't getting passed into 2nd level nested serializer")                  return "success" @@ -1494,11 +1515,11 @@ class NestedSerializerContextTests(TestCase):                  model = Album                  fields = ("photo_set", "callable") -            photo_set = PhotoSerializer(source="photo_set") +            photo_set = PhotoSerializer(source="photo_set", many=True)              callable = serializers.SerializerMethodField("_callable")              def _callable(self, instance): -                if not 'context_item' in self.context: +                if 'context_item' not in self.context:                      raise RuntimeError("context isn't getting passed into 1st level nested serializer")                  return "success" @@ -1506,7 +1527,7 @@ class NestedSerializerContextTests(TestCase):              albums = None          class AlbumCollectionSerializer(serializers.Serializer): -            albums = AlbumSerializer(source="albums") +            albums = AlbumSerializer(source="albums", many=True)          album1 = Album.objects.create(title="album 1")          album2 = Album.objects.create(title="album 2") @@ -1663,6 +1684,10 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase):              'url_field': [                  ('max_length', 1024),              ], +            'nullable_char_field': [ +                ('max_length', 1024), +                ('allow_none', True), +            ],          }      def field_test(self, field): @@ -1699,6 +1724,9 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase):      def test_url_field(self):          self.field_test('url_field') +    def test_nullable_char_field(self): +        self.field_test('nullable_char_field') +  @unittest.skipUnless(PIL is not None, 'PIL is not installed')  class DefaultValuesOnAutogeneratedFieldsTests(TestCase): @@ -1794,7 +1822,7 @@ class MetadataSerializerTestCase(TestCase):          self.assertEqual(expected, metadata) -### Regression test for #840 +# Regression test for #840  class SimpleModel(models.Model):      text = models.CharField(max_length=100) @@ -1828,7 +1856,7 @@ class FieldValidationRemovingAttr(TestCase):          self.assertEqual(serializer.object.text, 'foo') -### Regression test for #878 +# Regression test for #878  class SimpleTargetModel(models.Model):      text = models.CharField(max_length=100) diff --git a/rest_framework/tests/test_serializer_bulk_update.py b/tests/test_serializer_bulk_update.py index 8b0ded1a..67a8ed0d 100644 --- a/rest_framework/tests/test_serializer_bulk_update.py +++ b/tests/test_serializer_bulk_update.py @@ -83,9 +83,9 @@ class BulkCreateSerializerTests(TestCase):          self.assertEqual(serializer.is_valid(), False)          expected_errors = [ -                {'non_field_errors': ['Invalid data']}, -                {'non_field_errors': ['Invalid data']}, -                {'non_field_errors': ['Invalid data']} +            {'non_field_errors': ['Invalid data']}, +            {'non_field_errors': ['Invalid data']}, +            {'non_field_errors': ['Invalid data']}          ]          self.assertEqual(serializer.errors, expected_errors) diff --git a/rest_framework/tests/test_serializer_empty.py b/tests/test_serializer_empty.py index 30cff361..30cff361 100644 --- a/rest_framework/tests/test_serializer_empty.py +++ b/tests/test_serializer_empty.py diff --git a/rest_framework/tests/test_serializer_import.py b/tests/test_serializer_import.py index 9f30a7ff..3b8ff4b3 100644 --- a/rest_framework/tests/test_serializer_import.py +++ b/tests/test_serializer_import.py @@ -1,7 +1,7 @@  from django.test import TestCase  from rest_framework import serializers -from rest_framework.tests.accounts.serializers import AccountSerializer +from tests.accounts.serializers import AccountSerializer  class ImportingModelSerializerTests(TestCase): diff --git a/rest_framework/tests/test_serializer_nested.py b/tests/test_serializer_nested.py index 6d69ffbd..c09c24db 100644 --- a/rest_framework/tests/test_serializer_nested.py +++ b/tests/test_serializer_nested.py @@ -328,12 +328,14 @@ class NestedModelSerializerUpdateTests(TestCase):          class BlogPostSerializer(serializers.ModelSerializer):              comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set') +              class Meta:                  model = models.BlogPost                  fields = ('id', 'title', 'comments')          class PersonSerializer(serializers.ModelSerializer):              posts = BlogPostSerializer(many=True, source='blogpost_set') +              class Meta:                  model = models.Person                  fields = ('id', 'name', 'age', 'posts') diff --git a/rest_framework/tests/test_serializers.py b/tests/test_serializers.py index 120510ac..31c41730 100644 --- a/rest_framework/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -1,9 +1,7 @@ -from django.db import models  from django.test import TestCase - +from django.utils import six  from rest_framework.serializers import _resolve_model -from rest_framework.tests.models import BasicModel -from rest_framework.compat import six +from tests.models import BasicModel  class ResolveModelTests(TestCase): diff --git a/rest_framework/tests/test_settings.py b/tests/test_settings.py index 857375c2..e29fc34a 100644 --- a/rest_framework/tests/test_settings.py +++ b/tests/test_settings.py @@ -10,13 +10,13 @@ class TestSettings(TestCase):      def test_non_import_errors(self):          """Make sure other errors aren't suppressed.""" -        settings = APISettings({'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.tests.extras.bad_import.ModelSerializer'}, DEFAULTS, IMPORT_STRINGS) +        settings = APISettings({'DEFAULT_MODEL_SERIALIZER_CLASS': 'tests.extras.bad_import.ModelSerializer'}, DEFAULTS, IMPORT_STRINGS)          with self.assertRaises(ValueError):              settings.DEFAULT_MODEL_SERIALIZER_CLASS      def test_import_error_message_maintained(self):          """Make sure real import errors are captured and raised sensibly.""" -        settings = APISettings({'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.tests.extras.not_here.ModelSerializer'}, DEFAULTS, IMPORT_STRINGS) +        settings = APISettings({'DEFAULT_MODEL_SERIALIZER_CLASS': 'tests.extras.not_here.ModelSerializer'}, DEFAULTS, IMPORT_STRINGS)          with self.assertRaises(ImportError) as cm:              settings.DEFAULT_MODEL_SERIALIZER_CLASS          self.assertTrue('ImportError' in str(cm.exception)) diff --git a/rest_framework/tests/test_status.py b/tests/test_status.py index 7b1bdae3..721a6e30 100644 --- a/rest_framework/tests/test_status.py +++ b/tests/test_status.py @@ -30,4 +30,4 @@ class TestStatus(TestCase):          self.assertFalse(is_server_error(499))          self.assertTrue(is_server_error(500))          self.assertTrue(is_server_error(599)) -        self.assertFalse(is_server_error(600))
\ No newline at end of file +        self.assertFalse(is_server_error(600)) diff --git a/rest_framework/tests/test_templatetags.py b/tests/test_templatetags.py index d4da0c23..75ee0eaa 100644 --- a/rest_framework/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -11,7 +11,7 @@ class TemplateTagTests(TestCase):      def test_add_query_param_with_non_latin_charactor(self):          # Ensure we don't double-escape non-latin characters -        # that are present in the querystring. +        # that are present in the querystring.          # See #1314.          request = factory.get("/", {'q': '查询'})          json_url = add_query_param(request, "format", "json") @@ -48,4 +48,4 @@ class Issue1386Tests(TestCase):              self.assertEqual(i, res)          # example from issue #1386, this shouldn't raise an exception -        _ = urlize_quoted_links("asdf:[/p]zxcv.com") +        urlize_quoted_links("asdf:[/p]zxcv.com") diff --git a/rest_framework/tests/test_testing.py b/tests/test_testing.py index b16d1962..9c472026 100644 --- a/rest_framework/tests/test_testing.py +++ b/tests/test_testing.py @@ -1,11 +1,11 @@  # -- coding: utf-8 --  from __future__ import unicode_literals +from django.conf.urls import patterns, url  from io import BytesIO  from django.contrib.auth.models import User  from django.test import TestCase -from rest_framework.compat import patterns, url  from rest_framework.decorators import api_view  from rest_framework.response import Response  from rest_framework.test import APIClient, APIRequestFactory, force_authenticate @@ -28,14 +28,15 @@ def session_view(request):      }) -urlpatterns = patterns('', +urlpatterns = patterns( +    '',      url(r'^view/$', view),      url(r'^session-view/$', session_view),  )  class TestAPITestClient(TestCase): -    urls = 'rest_framework.tests.test_testing' +    urls = 'tests.test_testing'      def setUp(self):          self.client = APIClient() @@ -142,7 +143,8 @@ class TestAPIRequestFactory(TestCase):          assertion error.          """          factory = APIRequestFactory() -        self.assertRaises(AssertionError, factory.post, +        self.assertRaises( +            AssertionError, factory.post,              path='/view/', data={'example': 1}, format='xml'          ) diff --git a/rest_framework/tests/test_throttling.py b/tests/test_throttling.py index 41bff692..b0cb2fe7 100644 --- a/rest_framework/tests/test_throttling.py +++ b/tests/test_throttling.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals  from django.test import TestCase  from django.contrib.auth.models import User  from django.core.cache import cache +from rest_framework.settings import api_settings  from rest_framework.test import APIRequestFactory  from rest_framework.views import APIView  from rest_framework.throttling import BaseThrottle, UserRateThrottle, ScopedRateThrottle @@ -26,7 +27,7 @@ class NonTimeThrottle(BaseThrottle):          if not hasattr(self.__class__, 'called'):              self.__class__.called = True              return True -        return False  +        return False  class MockView(APIView): @@ -124,36 +125,42 @@ class ThrottlingTests(TestCase):          """          Ensure for second based throttles.          """ -        self.ensure_response_header_contains_proper_throttle_field(MockView, -         ((0, None), -          (0, None), -          (0, None), -          (0, '1') -         )) +        self.ensure_response_header_contains_proper_throttle_field( +            MockView, ( +                (0, None), +                (0, None), +                (0, None), +                (0, '1') +            ) +        )      def test_minutes_fields(self):          """          Ensure for minute based throttles.          """ -        self.ensure_response_header_contains_proper_throttle_field(MockView_MinuteThrottling, -         ((0, None), -          (0, None), -          (0, None), -          (0, '60') -         )) +        self.ensure_response_header_contains_proper_throttle_field( +            MockView_MinuteThrottling, ( +                (0, None), +                (0, None), +                (0, None), +                (0, '60') +            ) +        )      def test_next_rate_remains_constant_if_followed(self):          """          If a client follows the recommended next request rate,          the throttling rate should stay constant.          """ -        self.ensure_response_header_contains_proper_throttle_field(MockView_MinuteThrottling, -         ((0, None), -          (20, None), -          (40, None), -          (60, None), -          (80, None) -         )) +        self.ensure_response_header_contains_proper_throttle_field( +            MockView_MinuteThrottling, ( +                (0, None), +                (20, None), +                (40, None), +                (60, None), +                (80, None) +            ) +        )      def test_non_time_throttle(self):          """ @@ -169,7 +176,7 @@ class ThrottlingTests(TestCase):          self.assertTrue(MockView_NonTimeThrottling.throttle_classes[0].called)          response = MockView_NonTimeThrottling.as_view()(request) -        self.assertFalse('X-Throttle-Wait-Seconds' in response)  +        self.assertFalse('X-Throttle-Wait-Seconds' in response)  class ScopedRateThrottleTests(TestCase): @@ -275,3 +282,68 @@ class ScopedRateThrottleTests(TestCase):              self.increment_timer()              response = self.unscoped_view(request)              self.assertEqual(200, response.status_code) + + +class XffTestingBase(TestCase): +    def setUp(self): + +        class Throttle(ScopedRateThrottle): +            THROTTLE_RATES = {'test_limit': '1/day'} +            TIMER_SECONDS = 0 +            timer = lambda self: self.TIMER_SECONDS + +        class View(APIView): +            throttle_classes = (Throttle,) +            throttle_scope = 'test_limit' + +            def get(self, request): +                return Response('test_limit') + +        cache.clear() +        self.throttle = Throttle() +        self.view = View.as_view() +        self.request = APIRequestFactory().get('/some_uri') +        self.request.META['REMOTE_ADDR'] = '3.3.3.3' +        self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 1.1.1.1, 2.2.2.2' + +    def config_proxy(self, num_proxies): +        setattr(api_settings, 'NUM_PROXIES', num_proxies) + + +class IdWithXffBasicTests(XffTestingBase): +    def test_accepts_request_under_limit(self): +        self.config_proxy(0) +        self.assertEqual(200, self.view(self.request).status_code) + +    def test_denies_request_over_limit(self): +        self.config_proxy(0) +        self.view(self.request) +        self.assertEqual(429, self.view(self.request).status_code) + + +class XffSpoofingTests(XffTestingBase): +    def test_xff_spoofing_doesnt_change_machine_id_with_one_app_proxy(self): +        self.config_proxy(1) +        self.view(self.request) +        self.request.META['HTTP_X_FORWARDED_FOR'] = '4.4.4.4, 5.5.5.5, 2.2.2.2' +        self.assertEqual(429, self.view(self.request).status_code) + +    def test_xff_spoofing_doesnt_change_machine_id_with_two_app_proxies(self): +        self.config_proxy(2) +        self.view(self.request) +        self.request.META['HTTP_X_FORWARDED_FOR'] = '4.4.4.4, 1.1.1.1, 2.2.2.2' +        self.assertEqual(429, self.view(self.request).status_code) + + +class XffUniqueMachinesTest(XffTestingBase): +    def test_unique_clients_are_counted_independently_with_one_proxy(self): +        self.config_proxy(1) +        self.view(self.request) +        self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 1.1.1.1, 7.7.7.7' +        self.assertEqual(200, self.view(self.request).status_code) + +    def test_unique_clients_are_counted_independently_with_two_proxies(self): +        self.config_proxy(2) +        self.view(self.request) +        self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 7.7.7.7, 2.2.2.2' +        self.assertEqual(200, self.view(self.request).status_code) diff --git a/rest_framework/tests/test_urlizer.py b/tests/test_urlizer.py index 3dc8e8fe..a77aa22a 100644 --- a/rest_framework/tests/test_urlizer.py +++ b/tests/test_urlizer.py @@ -1,7 +1,6 @@  from __future__ import unicode_literals  from django.test import TestCase  from rest_framework.templatetags.rest_framework import urlize_quoted_links -import sys  class URLizerTests(TestCase): diff --git a/rest_framework/tests/test_urlpatterns.py b/tests/test_urlpatterns.py index 8132ec4c..e0060e69 100644 --- a/rest_framework/tests/test_urlpatterns.py +++ b/tests/test_urlpatterns.py @@ -1,9 +1,9 @@  from __future__ import unicode_literals  from collections import namedtuple +from django.conf.urls import patterns, url, include  from django.core import urlresolvers  from django.test import TestCase  from rest_framework.test import APIRequestFactory -from rest_framework.compat import patterns, url, include  from rest_framework.urlpatterns import format_suffix_patterns diff --git a/rest_framework/tests/test_validation.py b/tests/test_validation.py index e13e4078..e13e4078 100644 --- a/rest_framework/tests/test_validation.py +++ b/tests/test_validation.py diff --git a/rest_framework/tests/test_views.py b/tests/test_views.py index 77b113ee..77b113ee 100644 --- a/rest_framework/tests/test_views.py +++ b/tests/test_views.py diff --git a/rest_framework/tests/test_write_only_fields.py b/tests/test_write_only_fields.py index aabb18d6..aabb18d6 100644 --- a/rest_framework/tests/test_write_only_fields.py +++ b/tests/test_write_only_fields.py diff --git a/tests/urls.py b/tests/urls.py new file mode 100644 index 00000000..41f527df --- /dev/null +++ b/tests/urls.py @@ -0,0 +1,6 @@ +""" +Blank URLConf just to keep the test suite happy +""" +from django.conf.urls import patterns + +urlpatterns = patterns('') diff --git a/rest_framework/tests/users/__init__.py b/tests/users/__init__.py index e69de29b..e69de29b 100644 --- a/rest_framework/tests/users/__init__.py +++ b/tests/users/__init__.py diff --git a/rest_framework/tests/users/models.py b/tests/users/models.py index 128bac90..128bac90 100644 --- a/rest_framework/tests/users/models.py +++ b/tests/users/models.py diff --git a/rest_framework/tests/users/serializers.py b/tests/users/serializers.py index da496554..4893ddb3 100644 --- a/rest_framework/tests/users/serializers.py +++ b/tests/users/serializers.py @@ -1,6 +1,6 @@  from rest_framework import serializers -from rest_framework.tests.users.models import User +from tests.users.models import User  class UserSerializer(serializers.ModelSerializer): diff --git a/rest_framework/tests/utils.py b/tests/utils.py index a8f2eb0b..28be81bd 100644 --- a/rest_framework/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,5 @@  from contextlib import contextmanager -from rest_framework.compat import six +from django.utils import six  from rest_framework.settings import api_settings diff --git a/rest_framework/tests/views.py b/tests/views.py index 3917b74a..55935e92 100644 --- a/rest_framework/tests/views.py +++ b/tests/views.py @@ -1,6 +1,6 @@  from rest_framework import generics -from rest_framework.tests.models import NullableForeignKeySource -from rest_framework.tests.serializers import NullableFKSourceSerializer +from .models import NullableForeignKeySource +from .serializers import NullableFKSourceSerializer  class NullableFKSourceDetail(generics.RetrieveUpdateDestroyAPIView): @@ -1,14 +1,20 @@  [tox]  downloadcache = {toxworkdir}/cache/  envlist = +       flake8,         py3.4-django1.7,py3.3-django1.7,py3.2-django1.7,py2.7-django1.7,         py3.4-django1.6,py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,         py3.4-django1.5,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5, -       py2.7-django1.4,py2.6-django1.4, -       py2.7-django1.3,py2.6-django1.3 +       py2.7-django1.4,py2.6-django1.4  [testenv] -commands = {envpython} rest_framework/runtests/runtests.py +commands = ./runtests.py --fast + +[testenv:flake8] +basepython = python2.7 +deps = pytest==2.5.2 +       flake8==2.2.2 +commands = ./runtests.py --lintonly  [testenv:py3.4-django1.7]  basepython = python3.4 @@ -16,6 +22,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/         django-filter==0.7         defusedxml==0.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py3.3-django1.7]  basepython = python3.3 @@ -23,6 +30,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/         django-filter==0.7         defusedxml==0.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py3.2-django1.7]  basepython = python3.2 @@ -30,6 +38,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/         django-filter==0.7         defusedxml==0.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py2.7-django1.7]  basepython = python2.7 @@ -41,6 +50,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/         # django-oauth2-provider==0.2.4         django-guardian==1.2.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py3.4-django1.6]  basepython = python3.4 @@ -48,6 +58,7 @@ deps = Django==1.6.3         django-filter==0.7         defusedxml==0.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py3.3-django1.6]  basepython = python3.3 @@ -55,6 +66,7 @@ deps = Django==1.6.3         django-filter==0.7         defusedxml==0.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py3.2-django1.6]  basepython = python3.2 @@ -62,6 +74,7 @@ deps = Django==1.6.3         django-filter==0.7         defusedxml==0.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py2.7-django1.6]  basepython = python2.7 @@ -73,6 +86,7 @@ deps = Django==1.6.3         django-oauth2-provider==0.2.4         django-guardian==1.2.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py2.6-django1.6]  basepython = python2.6 @@ -84,6 +98,7 @@ deps = Django==1.6.3         django-oauth2-provider==0.2.4         django-guardian==1.2.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py3.4-django1.5]  basepython = python3.4 @@ -91,6 +106,7 @@ deps = django==1.5.6         django-filter==0.7         defusedxml==0.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py3.3-django1.5]  basepython = python3.3 @@ -98,6 +114,7 @@ deps = django==1.5.6         django-filter==0.7         defusedxml==0.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py3.2-django1.5]  basepython = python3.2 @@ -105,6 +122,7 @@ deps = django==1.5.6         django-filter==0.7         defusedxml==0.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py2.7-django1.5]  basepython = python2.7 @@ -116,6 +134,7 @@ deps = django==1.5.6         django-oauth2-provider==0.2.3         django-guardian==1.2.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py2.6-django1.5]  basepython = python2.6 @@ -127,6 +146,7 @@ deps = django==1.5.6         django-oauth2-provider==0.2.3         django-guardian==1.2.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py2.7-django1.4]  basepython = python2.7 @@ -138,6 +158,7 @@ deps = django==1.4.11         django-oauth2-provider==0.2.3         django-guardian==1.2.3         Pillow==2.3.0 +       pytest-django==2.6.1  [testenv:py2.6-django1.4]  basepython = python2.6 @@ -149,25 +170,4 @@ deps = django==1.4.11         django-oauth2-provider==0.2.3         django-guardian==1.2.3         Pillow==2.3.0 - -[testenv:py2.7-django1.3] -basepython = python2.7 -deps = django==1.3.5 -       django-filter==0.5.4 -       defusedxml==0.3 -       django-oauth-plus==2.2.1 -       oauth2==1.5.211 -       django-oauth2-provider==0.2.3 -       django-guardian==1.2.3 -       Pillow==2.3.0 - -[testenv:py2.6-django1.3] -basepython = python2.6 -deps = django==1.3.5 -       django-filter==0.5.4 -       defusedxml==0.3 -       django-oauth-plus==2.2.1 -       oauth2==1.5.211 -       django-oauth2-provider==0.2.3 -       django-guardian==1.2.3 -       Pillow==2.3.0 +       pytest-django==2.6.1 | 
