mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-10-31 07:57:55 +03:00 
			
		
		
		
	
						commit
						6ffc97c808
					
				
							
								
								
									
										15
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								.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" | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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. | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -51,13 +51,16 @@ 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 | ||||
|     from rest_framework.decorators import detail_route | ||||
|      | ||||
|     @action(permission_classes=[IsAdminOrIsSelf]) | ||||
|     class UserViewSet(ModelViewSet): | ||||
|         ... | ||||
|          | ||||
|         @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf]) | ||||
|         def set_password(self, request, pk=None): | ||||
|             ... | ||||
| 
 | ||||
|  | @ -65,22 +68,24 @@ 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}$', | ||||
|             Route( | ||||
|             	url=r'^{prefix}$', | ||||
|             	mapping={'get': 'list'}, | ||||
|             	name='{basename}-list', | ||||
|                   initkwargs={'suffix': 'List'}), | ||||
|             Route(url=r'^{prefix}/{lookup}$', | ||||
|             	initkwargs={'suffix': 'List'} | ||||
|             ), | ||||
|             Route( | ||||
|             	url=r'^{prefix}/{lookup}$', | ||||
|                mapping={'get': 'retrieve'}, | ||||
|                name='{basename}-detail', | ||||
|                   initkwargs={'suffix': '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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) | ||||
| 
 | ||||
|         @action(permission_classes=[IsAdminOrIsSelf]) | ||||
| The decorators can additionally take extra arguments that will be set for the routed view only.  For example... | ||||
| 
 | ||||
|         @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf]) | ||||
|         def set_password(self, request, pk=None): | ||||
|            ... | ||||
| 
 | ||||
| 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 | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								docs/img/labels-and-milestones.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/img/labels-and-milestones.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 82 KiB | 
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										147
									
								
								docs/topics/2.4-accouncement.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								docs/topics/2.4-accouncement.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,147 @@ | |||
| # REST framework 2.4 announcement | ||||
| 
 | ||||
| The 2.4 release is largely an intermediate step, tying up some outstanding issues prior to the 3.x series. | ||||
| 
 | ||||
| ## Version requirements | ||||
| 
 | ||||
| Support for Django 1.3 has been dropped. | ||||
| The lowest supported version of Django is now 1.4.2. | ||||
| 
 | ||||
| The current plan is for REST framework to remain in lockstep with [Django's long-term support policy][lts-releases]. | ||||
| 
 | ||||
| ## Django 1.7 support | ||||
| 
 | ||||
| The optional authtoken application now includes support for *both* Django 1.7 schema migrations, *and* for old-style `south` migrations. | ||||
| 
 | ||||
| **If you are using authtoken, and you want to continue using `south`, you must upgrade your `south` package to version 1.0.** | ||||
| 
 | ||||
| ## Updated test runner | ||||
| 
 | ||||
| We now have a new test runner for developing against the project,, that uses the excellent [py.test](http://pytest.org) library. | ||||
| 
 | ||||
| To use it make sure you have first installed the test requirements. | ||||
| 
 | ||||
|     pip install -r requirements-test.txt | ||||
| 
 | ||||
| Then run the `runtests.py` script. | ||||
| 
 | ||||
|     ./runtests.py | ||||
| 
 | ||||
| The new test runner also includes [flake8](https://flake8.readthedocs.org) code linting, which should help keep our coding style consistent. | ||||
| 
 | ||||
| #### Test runner flags | ||||
| 
 | ||||
| Run using a more concise output style. | ||||
| 
 | ||||
|     ./runtests -q | ||||
| 
 | ||||
| Run the tests using a more concise output style, no coverage, no flake8. | ||||
| 
 | ||||
|     ./runtests --fast | ||||
| 
 | ||||
| Don't run the flake8 code linting. | ||||
| 
 | ||||
|     ./runtests --nolint | ||||
| 
 | ||||
| Only run the flake8 code linting, don't run the tests. | ||||
| 
 | ||||
|     ./runtests --lintonly | ||||
| 
 | ||||
| Run the tests for a given test case. | ||||
| 
 | ||||
|     ./runtests MyTestCase | ||||
| 
 | ||||
| Run the tests for a given test method. | ||||
| 
 | ||||
|     ./runtests MyTestCase.test_this_method | ||||
| 
 | ||||
| Shorter form to run the tests for a given test method. | ||||
| 
 | ||||
|     ./runtests test_this_method | ||||
| 
 | ||||
| Note: The test case and test method matching is fuzzy and will sometimes run other tests that contain a partial string match to the given  command line input. | ||||
| 
 | ||||
| ## Improved viewset routing | ||||
| 
 | ||||
| The `@action` and `@link` decorators were inflexible in that they only allowed additional routes to be added against instance style URLs, not against list style URLs. | ||||
| 
 | ||||
| The `@action` and `@link` decorators have now been moved to pending deprecation, and the `@list_route` and `@detail_route` decroators have been introduced. | ||||
| 
 | ||||
| Here's an example of using the new decorators. Firstly we have a detail-type route named "set_password" that acts on a single instance, and takes a `pk` argument in the URL. Secondly we have a list-type route named "recent_users" that acts on a queryset, and does not take any arguments in the URL. | ||||
| 
 | ||||
|     class UserViewSet(viewsets.ModelViewSet): | ||||
|         """ | ||||
|         A viewset that provides the standard actions | ||||
|         """ | ||||
|         queryset = User.objects.all() | ||||
|         serializer_class = UserSerializer | ||||
| 
 | ||||
|         @detail_route(methods=['post']) | ||||
|         def set_password(self, request, pk=None): | ||||
|             user = self.get_object() | ||||
|             serializer = PasswordSerializer(data=request.DATA) | ||||
|             if serializer.is_valid(): | ||||
|                 user.set_password(serializer.data['password']) | ||||
|                 user.save() | ||||
|                 return Response({'status': 'password set'}) | ||||
|             else: | ||||
|                 return Response(serializer.errors, | ||||
|                                 status=status.HTTP_400_BAD_REQUEST) | ||||
| 
 | ||||
|         @list_route() | ||||
|         def recent_users(self, request): | ||||
|             recent_users = User.objects.all().order('-last_login') | ||||
|             page = self.paginate_queryset(recent_users) | ||||
|             serializer = self.get_pagination_serializer(page) | ||||
|             return Response(serializer.data) | ||||
| 
 | ||||
| For more details, see the [viewsets documentation](../api-guide/viewsets.md). | ||||
| 
 | ||||
| ## Other features | ||||
| 
 | ||||
| There are also a number of other features and bugfixes as [listed in the release notes][2-4-release-notes]. In particular these include: | ||||
| 
 | ||||
| [Customizable view name and description functions][view-name-and-description-settings] for use with the browsable API, by using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. | ||||
| 
 | ||||
| Smarter [client IP identification for throttling][client-ip-identification], with the addition of the `NUM_PROXIES` setting. | ||||
| 
 | ||||
| ## Deprecations | ||||
| 
 | ||||
| All API changes in 2.3 that previously raised `PendingDeprecationWarning` will now raise a `DeprecationWarning`, which is loud by default. | ||||
| 
 | ||||
| All API changes in 2.3 that previously raised `DeprecationWarning` have now been removed entirely. | ||||
| 
 | ||||
| Furter details on these deprecations is available in the [2.3 announcement][2-3-announcement]. | ||||
| 
 | ||||
| ## Labels and milestones | ||||
| 
 | ||||
| Although not strictly part of the 2.4 release it's also worth noting here that we've been working hard towards improving our triage process. | ||||
| 
 | ||||
| The [labels that we use in GitHub][github-labels] have been cleaned up, and all existing tickets triaged. Any given ticket should have one and only one label, indicating its current state. | ||||
| 
 | ||||
| We've also [started using milestones][github-milestones] in order to track tickets against particular releases. | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| **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 | ||||
|  | @ -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: | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -1,7 +1,7 @@ | |||
| """ | ||||
| ______ _____ _____ _____    __                                             _     | ||||
| ______ _____ _____ _____    __ | ||||
| | ___ \  ___/  ___|_   _|  / _|                                           | | | ||||
| | |_/ / |__ \ `--.  | |   | |_ _ __ __ _ _ __ ___   _____      _____  _ __| | __ | ||||
| | |_/ / |__ \ `--.  | |   | |_ _ __ __ _ _ __ ___   _____      _____  _ __| |__ | ||||
| |    /|  __| `--. \ | |   |  _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ / | ||||
| | |\ \| |___/\__/ / | |   | | | | | (_| | | | | | |  __/\ V  V / (_) | |  |   < | ||||
| \_| \_\____/\____/  \_/   |_| |_|  \__,_|_| |_| |_|\___| \_/\_/ \___/|_|  |_|\_| | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -1,67 +1,27 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| import datetime | ||||
| from south.db import db | ||||
| from south.v2 import SchemaMigration | ||||
| from django.db import models | ||||
| # encoding: utf8 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| from rest_framework.settings import api_settings | ||||
| from django.db import models, migrations | ||||
| from django.conf import 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(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         migrations.swappable_dependency(settings.AUTH_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'}) | ||||
|     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, | ||||
|             }, | ||||
|         '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'] | ||||
|             bases=(models.Model,), | ||||
|         ), | ||||
|     ] | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| import binascii | ||||
| import os | ||||
| from hashlib import sha1 | ||||
| from django.conf import settings | ||||
| from django.db import models | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										60
									
								
								rest_framework/authtoken/south_migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								rest_framework/authtoken/south_migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,60 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| from south.db import db | ||||
| from south.v2 import SchemaMigration | ||||
| 
 | ||||
| try: | ||||
|     from django.contrib.auth import get_user_model | ||||
| except ImportError:  # django < 1.5 | ||||
|     from django.contrib.auth.models import User | ||||
| else: | ||||
|     User = get_user_model() | ||||
| 
 | ||||
| 
 | ||||
| class Migration(SchemaMigration): | ||||
| 
 | ||||
|     def forwards(self, orm): | ||||
|         # Adding model 'Token' | ||||
|         db.create_table('authtoken_token', ( | ||||
|             ('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)), | ||||
|             ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])), | ||||
|             ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), | ||||
|         )) | ||||
|         db.send_create_signal('authtoken', ['Token']) | ||||
| 
 | ||||
|     def backwards(self, orm): | ||||
|         # Deleting model 'Token' | ||||
|         db.delete_table('authtoken_token') | ||||
| 
 | ||||
|     models = { | ||||
|         'auth.group': { | ||||
|             'Meta': {'object_name': 'Group'}, | ||||
|             'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||||
|             'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), | ||||
|             'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) | ||||
|         }, | ||||
|         'auth.permission': { | ||||
|             'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, | ||||
|             'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||||
|             'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), | ||||
|             'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||||
|             'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) | ||||
|         }, | ||||
|         "%s.%s" % (User._meta.app_label, User._meta.module_name): { | ||||
|             'Meta': {'object_name': User._meta.module_name}, | ||||
|         }, | ||||
|         'authtoken.token': { | ||||
|             'Meta': {'object_name': 'Token'}, | ||||
|             'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), | ||||
|             'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}), | ||||
|             'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)}) | ||||
|         }, | ||||
|         'contenttypes.contenttype': { | ||||
|             'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, | ||||
|             'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||||
|             'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||||
|             'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||||
|             'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     complete_apps = ['authtoken'] | ||||
|  | @ -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 | ||||
|     from django.views.generic import View as DjangoView | ||||
| 
 | ||||
|     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 | ||||
|     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: | ||||
|  |  | |||
|  | @ -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 link(**kwargs): | ||||
| def detail_route(methods=['get'], **kwargs): | ||||
|     """ | ||||
|     Used to mark a method on a ViewSet that should be routed for GET requests. | ||||
|     Used to mark a method on a ViewSet that should be routed for detail requests. | ||||
|     """ | ||||
|     def decorator(func): | ||||
|         func.bind_to_methods = ['get'] | ||||
|         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 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 | ||||
|  |  | |||
|  | @ -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.' | ||||
|  |  | |||
|  | @ -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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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') % { | ||||
|         except InvalidPage as exc: | ||||
|             error_format = _('Invalid page (%(page_number)s): %(message)s') | ||||
|             raise Http404(error_format % { | ||||
|                 'page_number': page_number, | ||||
|                                 'message': str(e) | ||||
|                 '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) | ||||
|  |  | |||
|  | @ -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} | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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  | ||||
|         return ( | ||||
|             request.method in SAFE_METHODS or | ||||
|             request.user and | ||||
|             request.user.is_authenticated()) | ||||
|             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' | ||||
|         assert False, ( | ||||
|             'TokenHasReadWriteScope requires either the' | ||||
|             '`OAuthAuthentication` or `OAuth2Authentication` authentication ' | ||||
|         'class to be used.') | ||||
|             'class to be used.' | ||||
|         ) | ||||
|  |  | |||
|  | @ -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 " | ||||
|         assert request is not None, ( | ||||
|             "`HyperlinkedRelatedField` requires the request in the serializer " | ||||
|             "context. Add `context={'request': request}` when instantiating " | ||||
|             "the serializer." | ||||
|         ) | ||||
|             warnings.warn(msg, DeprecationWarning, stacklevel=4) | ||||
| 
 | ||||
|         # If the object has not yet been saved then we cannot hyperlink to it. | ||||
|         if 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) | ||||
|  |  | |||
|  | @ -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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 ' \ | ||||
|             assert charset, ( | ||||
|                 'renderer returned unicode, and did not specify ' | ||||
|                 'a charset value.' | ||||
|             ) | ||||
|             return bytes(ret.encode(charset)) | ||||
| 
 | ||||
|         if not ret: | ||||
|  |  | |||
|  | @ -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): | ||||
|         """ | ||||
|  |  | |||
|  | @ -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() | ||||
|  | @ -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() | ||||
|  | @ -1,7 +0,0 @@ | |||
| """ | ||||
| Blank URLConf just to keep runtests.py happy. | ||||
| """ | ||||
| from rest_framework.compat import patterns | ||||
| 
 | ||||
| urlpatterns = patterns('', | ||||
| ) | ||||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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")) | ||||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| {% load url from future %} | ||||
| {% load staticfiles %} | ||||
| {% load rest_framework %} | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| {% extends "rest_framework/base.html" %} | ||||
| {% load url from future %} | ||||
| {% load staticfiles %} | ||||
| {% load rest_framework %} | ||||
| 
 | ||||
|     {% block body %} | ||||
|  |  | |||
|  | @ -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') | ||||
|  |  | |||
|  | @ -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()]) | ||||
|                 ) | ||||
|  |  | |||
|  | @ -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', '/')]) | ||||
|  | @ -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) | ||||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,12 +13,14 @@ 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') | ||||
| ) | ||||
|  |  | |||
|  | @ -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 | ||||
|     ) | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
							
								
								
									
										86
									
								
								runtests.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										86
									
								
								runtests.py
									
									
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,86 @@ | |||
| #! /usr/bin/env python | ||||
| from __future__ import print_function | ||||
| 
 | ||||
| import pytest | ||||
| import sys | ||||
| import os | ||||
| import subprocess | ||||
| 
 | ||||
| 
 | ||||
| PYTEST_ARGS = { | ||||
|     'default': ['tests'], | ||||
|     'fast': ['tests', '-q'], | ||||
| } | ||||
| 
 | ||||
| FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501'] | ||||
| 
 | ||||
| 
 | ||||
| sys.path.append(os.path.dirname(__file__)) | ||||
| 
 | ||||
| def exit_on_failure(ret, message=None): | ||||
|     if ret: | ||||
|         sys.exit(ret) | ||||
| 
 | ||||
| def flake8_main(args): | ||||
|     print('Running flake8 code linting') | ||||
|     ret = subprocess.call(['flake8'] + args) | ||||
|     print('flake8 failed' if ret else 'flake8 passed') | ||||
|     return ret | ||||
| 
 | ||||
| def split_class_and_function(string): | ||||
|     class_string, function_string = string.split('.', 1) | ||||
|     return "%s and %s" % (class_string, function_string) | ||||
| 
 | ||||
| def is_function(string): | ||||
|     # `True` if it looks like a test function is included in the string. | ||||
|     return string.startswith('test_') or '.test_' in string | ||||
| 
 | ||||
| def is_class(string): | ||||
|     # `True` if first character is uppercase - assume it's a class name. | ||||
|     return string[0] == string[0].upper() | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     try: | ||||
|         sys.argv.remove('--nolint') | ||||
|     except ValueError: | ||||
|         run_flake8 = True | ||||
|     else: | ||||
|         run_flake8 = False | ||||
| 
 | ||||
|     try: | ||||
|         sys.argv.remove('--lintonly') | ||||
|     except ValueError: | ||||
|         run_tests = True | ||||
|     else: | ||||
|         run_tests = False | ||||
| 
 | ||||
|     try: | ||||
|         sys.argv.remove('--fast') | ||||
|     except ValueError: | ||||
|         style = 'default' | ||||
|     else: | ||||
|         style = 'fast' | ||||
|         run_flake8 = False | ||||
| 
 | ||||
|     if len(sys.argv) > 1: | ||||
|         pytest_args = sys.argv[1:] | ||||
|         first_arg = pytest_args[0] | ||||
|         if first_arg.startswith('-'): | ||||
|             # `runtests.py [flags]` | ||||
|             pytest_args = ['tests'] + pytest_args | ||||
|         elif is_class(first_arg) and is_function(first_arg): | ||||
|             # `runtests.py TestCase.test_function [flags]` | ||||
|             expression = split_class_and_function(first_arg) | ||||
|             pytest_args = ['tests', '-k', expression] + pytest_args[1:] | ||||
|         elif is_class(first_arg) or is_function(first_arg): | ||||
|             # `runtests.py TestCase [flags]`  | ||||
|             # `runtests.py test_function [flags]` | ||||
|             pytest_args = ['tests', '-k', pytest_args[0]] + pytest_args[1:] | ||||
|     else: | ||||
|         pytest_args = PYTEST_ARGS[style] | ||||
| 
 | ||||
|     if run_tests: | ||||
|         exit_on_failure(pytest.main(pytest_args)) | ||||
|     if run_flake8: | ||||
|         exit_on_failure(flake8_main(FLAKE8_ARGS)) | ||||
							
								
								
									
										17
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								setup.py
									
									
									
									
									
								
							|  | @ -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', | ||||
|  |  | |||
|  | @ -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): | ||||
|  | @ -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): | ||||
							
								
								
									
										88
									
								
								tests/conftest.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								tests/conftest.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,88 @@ | |||
| def pytest_configure(): | ||||
|     from django.conf import settings | ||||
| 
 | ||||
|     settings.configure( | ||||
|         DEBUG_PROPAGATE_EXCEPTIONS=True, | ||||
|         DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3', | ||||
|                                'NAME': ':memory:'}}, | ||||
|         SECRET_KEY='not very secret in tests', | ||||
|         USE_I18N=True, | ||||
|         USE_L10N=True, | ||||
|         STATIC_URL='/static/', | ||||
|         ROOT_URLCONF='tests.urls', | ||||
|         TEMPLATE_LOADERS=( | ||||
|             'django.template.loaders.filesystem.Loader', | ||||
|             'django.template.loaders.app_directories.Loader', | ||||
|         ), | ||||
|         MIDDLEWARE_CLASSES=( | ||||
|             'django.middleware.common.CommonMiddleware', | ||||
|             'django.contrib.sessions.middleware.SessionMiddleware', | ||||
|             'django.middleware.csrf.CsrfViewMiddleware', | ||||
|             'django.contrib.auth.middleware.AuthenticationMiddleware', | ||||
|             'django.contrib.messages.middleware.MessageMiddleware', | ||||
|         ), | ||||
|         INSTALLED_APPS=( | ||||
|             'django.contrib.auth', | ||||
|             'django.contrib.contenttypes', | ||||
|             'django.contrib.sessions', | ||||
|             'django.contrib.sites', | ||||
|             'django.contrib.messages', | ||||
|             'django.contrib.staticfiles', | ||||
| 
 | ||||
|             'rest_framework', | ||||
|             'rest_framework.authtoken', | ||||
|             'tests', | ||||
|             'tests.accounts', | ||||
|             'tests.records', | ||||
|             'tests.users', | ||||
|         ), | ||||
|         PASSWORD_HASHERS=( | ||||
|             'django.contrib.auth.hashers.SHA1PasswordHasher', | ||||
|             'django.contrib.auth.hashers.PBKDF2PasswordHasher', | ||||
|             'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', | ||||
|             'django.contrib.auth.hashers.BCryptPasswordHasher', | ||||
|             'django.contrib.auth.hashers.MD5PasswordHasher', | ||||
|             'django.contrib.auth.hashers.CryptPasswordHasher', | ||||
|         ), | ||||
|     ) | ||||
| 
 | ||||
|     try: | ||||
|         import oauth_provider  # NOQA | ||||
|         import oauth2  # NOQA | ||||
|     except ImportError: | ||||
|         pass | ||||
|     else: | ||||
|         settings.INSTALLED_APPS += ( | ||||
|             'oauth_provider', | ||||
|         ) | ||||
| 
 | ||||
|     try: | ||||
|         import provider  # NOQA | ||||
|     except ImportError: | ||||
|         pass | ||||
|     else: | ||||
|         settings.INSTALLED_APPS += ( | ||||
|             'provider', | ||||
|             'provider.oauth2', | ||||
|         ) | ||||
| 
 | ||||
|     # guardian is optional | ||||
|     try: | ||||
|         import guardian  # NOQA | ||||
|     except ImportError: | ||||
|         pass | ||||
|     else: | ||||
|         settings.ANONYMOUS_USER_ID = -1 | ||||
|         settings.AUTHENTICATION_BACKENDS = ( | ||||
|             'django.contrib.auth.backends.ModelBackend', | ||||
|             'guardian.backends.ObjectPermissionBackend', | ||||
|         ) | ||||
|         settings.INSTALLED_APPS += ( | ||||
|             'guardian', | ||||
|         ) | ||||
| 
 | ||||
|     try: | ||||
|         import django | ||||
|         django.setup() | ||||
|     except AttributeError: | ||||
|         pass | ||||
|  | @ -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() | ||||
|  | @ -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): | ||||
|  | @ -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,7 +124,7 @@ else: | |||
| 
 | ||||
| # guardian is optional | ||||
| try: | ||||
|     import guardian | ||||
|     import guardian  # NOQA | ||||
| except ImportError: | ||||
|     pass | ||||
| else: | ||||
|  | @ -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) | ||||
|  | @ -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') | ||||
							
								
								
									
										100
									
								
								tests/test_breadcrumbs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								tests/test_breadcrumbs.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,100 @@ | |||
| from __future__ import unicode_literals | ||||
| from django.conf.urls import patterns, url | ||||
| from django.test import TestCase | ||||
| from rest_framework.utils.breadcrumbs import get_breadcrumbs | ||||
| from rest_framework.views import APIView | ||||
| 
 | ||||
| 
 | ||||
| class Root(APIView): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class ResourceRoot(APIView): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class ResourceInstance(APIView): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class NestedResourceRoot(APIView): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class NestedResourceInstance(APIView): | ||||
|     pass | ||||
| 
 | ||||
| urlpatterns = patterns( | ||||
|     '', | ||||
|     url(r'^$', Root.as_view()), | ||||
|     url(r'^resource/$', ResourceRoot.as_view()), | ||||
|     url(r'^resource/(?P<key>[0-9]+)$', ResourceInstance.as_view()), | ||||
|     url(r'^resource/(?P<key>[0-9]+)/$', NestedResourceRoot.as_view()), | ||||
|     url(r'^resource/(?P<key>[0-9]+)/(?P<other>[A-Za-z]+)$', NestedResourceInstance.as_view()), | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| class BreadcrumbTests(TestCase): | ||||
|     """Tests the breadcrumb functionality used by the HTML renderer.""" | ||||
| 
 | ||||
|     urls = 'tests.test_breadcrumbs' | ||||
| 
 | ||||
|     def test_root_breadcrumbs(self): | ||||
|         url = '/' | ||||
|         self.assertEqual( | ||||
|             get_breadcrumbs(url), | ||||
|             [('Root', '/')] | ||||
|         ) | ||||
| 
 | ||||
|     def test_resource_root_breadcrumbs(self): | ||||
|         url = '/resource/' | ||||
|         self.assertEqual( | ||||
|             get_breadcrumbs(url), | ||||
|             [ | ||||
|                 ('Root', '/'), | ||||
|                 ('Resource Root', '/resource/') | ||||
|             ] | ||||
|         ) | ||||
| 
 | ||||
|     def test_resource_instance_breadcrumbs(self): | ||||
|         url = '/resource/123' | ||||
|         self.assertEqual( | ||||
|             get_breadcrumbs(url), | ||||
|             [ | ||||
|                 ('Root', '/'), | ||||
|                 ('Resource Root', '/resource/'), | ||||
|                 ('Resource Instance', '/resource/123') | ||||
|             ] | ||||
|         ) | ||||
| 
 | ||||
|     def test_nested_resource_breadcrumbs(self): | ||||
|         url = '/resource/123/' | ||||
|         self.assertEqual( | ||||
|             get_breadcrumbs(url), | ||||
|             [ | ||||
|                 ('Root', '/'), | ||||
|                 ('Resource Root', '/resource/'), | ||||
|                 ('Resource Instance', '/resource/123'), | ||||
|                 ('Nested Resource Root', '/resource/123/') | ||||
|             ] | ||||
|         ) | ||||
| 
 | ||||
|     def test_nested_resource_instance_breadcrumbs(self): | ||||
|         url = '/resource/123/abc' | ||||
|         self.assertEqual( | ||||
|             get_breadcrumbs(url), | ||||
|             [ | ||||
|                 ('Root', '/'), | ||||
|                 ('Resource Root', '/resource/'), | ||||
|                 ('Resource Instance', '/resource/123'), | ||||
|                 ('Nested Resource Root', '/resource/123/'), | ||||
|                 ('Nested Resource Instance', '/resource/123/abc') | ||||
|             ] | ||||
|         ) | ||||
| 
 | ||||
|     def test_broken_url_breadcrumbs_handled_gracefully(self): | ||||
|         url = '/foobar' | ||||
|         self.assertEqual( | ||||
|             get_breadcrumbs(url), | ||||
|             [('Root', '/')] | ||||
|         ) | ||||
|  | @ -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 | ||||
|  | @ -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): | ||||
|  | @ -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()) | ||||
|  | @ -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(), | ||||
|  | @ -226,6 +238,18 @@ class IntegrationTestFiltering(CommonFilteringTestCase): | |||
|         request = factory.get('/') | ||||
|         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): | ||||
|         """ | ||||
|  | @ -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)) | ||||
|  | @ -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 | ||||
|  | @ -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() | ||||
| 
 | ||||
|  | @ -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): | ||||
|         """ | ||||
|  | @ -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): | ||||
|  | @ -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 | ||||
|  | @ -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) | ||||
|  | @ -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: | ||||
|  | @ -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) | ||||
|  | @ -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 | ||||
|  | @ -215,14 +228,6 @@ 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') | ||||
|  | @ -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) | ||||
| 
 | ||||
|  | @ -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') | ||||
| 
 | ||||
|  | @ -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): | ||||
|  | @ -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): | ||||
|         """ | ||||
|  | @ -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 | ||||
|  | @ -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): | ||||
|  | @ -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() | ||||
| 
 | ||||
|  | @ -335,8 +342,10 @@ class JSONRendererTests(TestCase): | |||
|         class DictLike(object): | ||||
|             def __init__(self): | ||||
|                 self._dict = {} | ||||
| 
 | ||||
|             def set(self, value): | ||||
|                 self._dict = dict(value) | ||||
| 
 | ||||
|             def __getitem__(self, key): | ||||
|                 return self._dict[key] | ||||
| 
 | ||||
|  | @ -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): | ||||
|  | @ -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) | ||||
|  | @ -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): | ||||
|         """ | ||||
|  | @ -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') | ||||
|  | @ -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) | ||||
|  | @ -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,8 +666,8 @@ 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.']}, {}]) | ||||
| 
 | ||||
|  | @ -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) | ||||
| 
 | ||||
|  | @ -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) | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user