aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xdocs/api-guide/authentication.md5
-rw-r--r--docs/api-guide/pagination.md164
-rw-r--r--docs/api-guide/testing.md4
-rw-r--r--docs/topics/third-party-resources.md2
-rw-r--r--rest_framework/fields.py65
-rw-r--r--rest_framework/metadata.py8
-rw-r--r--rest_framework/pagination.py6
-rw-r--r--rest_framework/routers.py40
-rw-r--r--rest_framework/test.py3
-rw-r--r--tests/test_metadata.py17
-rw-r--r--tests/test_routers.py14
11 files changed, 253 insertions, 75 deletions
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index 4b8110bd..fe1be7bf 100755
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -353,6 +353,10 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
[Djoser][djoser] library provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. The package works with a custom user model and it uses token based authentication. This is a ready to use REST implementation of Django authentication system.
+## django-rest-auth
+
+[Django-rest-auth][django-rest-auth] library provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc. By having these API endpoints, your client apps such as AngularJS, iOS, Android, and others can communicate to your Django backend site independently via REST APIs for user management.
+
[cite]: http://jacobian.org/writing/rest-worst-practices/
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
[http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
@@ -392,3 +396,4 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
[mohawk]: http://mohawk.readthedocs.org/en/latest/
[mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
[djoser]: https://github.com/sunscrapers/djoser
+[django-rest-auth]: https://github.com/Tivix/django-rest-auth
diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md
index bae579a6..697ba38d 100644
--- a/docs/api-guide/pagination.md
+++ b/docs/api-guide/pagination.md
@@ -32,14 +32,14 @@ You can also set the pagination class on an individual view by using the `pagina
If you want to modify particular aspects of the pagination style, you'll want to override one of the pagination classes, and set the attributes that you want to change.
class LargeResultsSetPagination(PageNumberPagination):
- paginate_by = 1000
- paginate_by_param = 'page_size'
- max_paginate_by = 10000
+ page_size = 1000
+ page_size_query_param = 'page_size'
+ max_page_size = 10000
class StandardResultsSetPagination(PageNumberPagination):
- paginate_by = 100
- paginate_by_param = 'page_size'
- max_paginate_by = 1000
+ page_size = 100
+ page_size_query_param = 'page_size'
+ max_page_size = 1000
You can then apply your new style to a view using the `.pagination_class` attribute:
@@ -59,15 +59,141 @@ Or apply the style globally, using the `DEFAULT_PAGINATION_CLASS` settings key.
## PageNumberPagination
-**TODO**
+This pagination style accepts a single number page number in the request query parameters.
+
+**Request**:
+
+ GET https://api.example.org/accounts/?page=4
+
+**Response**:
+
+ HTTP 200 OK
+ {
+ "count": 1023
+ "next": "https://api.example.org/accounts/?page=5",
+ "previous": "https://api.example.org/accounts/?page=3",
+ "results": [
+ …
+ ]
+ }
+
+#### Setup
+
+To enable the `PageNumberPagination` style globally, use the following configuration, modifying the `DEFAULT_PAGE_SIZE` as desired:
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
+ 'DEFAULT_PAGE_SIZE': 100
+ }
+
+On `GenericAPIView` subclasses you may also set the `pagination_class` attribute to select `PageNumberPagination` on a per-view basis.
+
+#### Configuration
+
+The `PageNumberPagination` class includes a number of attributes that may be overridden to modify the pagination style.
+
+To set these attributes you should override the `PageNumberPagination` class, and then enable your custom pagination class as above.
+
+* `page_size` - A numeric value indicating the page size. If set, this overrides the `DEFAULT_PAGE_SIZE` setting. Defaults to the same value as the `DEFAULT_PAGE_SIZE` settings key.
+* `page_query_param` - A string value indicating the name of the query parameter to use for the pagination control.
+* `page_size_query_param` - If set, this is a string value indicating the name of a query parameter that allows the client to set the page size on a per-request basis. Defaults to `None`, indicating that the client may not control the requested page size.
+* `max_page_size` - If set, this is a numeric value indicating the maximum allowable requested page size. This attribute is only valid if `page_size_query_param` is also set.
+* `last_page_strings` - A list or tuple of string values indicating values that may be used with the `page_query_param` to request the final page in the set. Defaults to `('last',)`
+* `template` - The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/numbers.html"`.
+
+---
## LimitOffsetPagination
-**TODO**
+This pagination style mirrors the syntax used when looking up multiple database records. The client includes both a "limit" and an
+"offset" query parameter. The limit indicates the maximum number of items to return, and is equivalent to the `page_size` in other styles. The offset indicates the starting position of the query in relation to the complete set of unpaginated items.
+
+**Request**:
+
+ GET https://api.example.org/accounts/?limit=100&offset=400
+
+**Response**:
+
+ HTTP 200 OK
+ {
+ "count": 1023
+ "next": "https://api.example.org/accounts/?limit=100&offset=500",
+ "previous": "https://api.example.org/accounts/?limit=100&offset=300",
+ "results": [
+ …
+ ]
+ }
+
+#### Setup
+
+To enable the `PageNumberPagination` style globally, use the following configuration:
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination'
+ }
+
+Optionally, you may also set a `DEFAULT_PAGE_SIZE` key. If the `DEFAULT_PAGE_SIZE` parameter is also used then the `limit` query parameter will be optional, and may be omitted by the client.
+
+On `GenericAPIView` subclasses you may also set the `pagination_class` attribute to select `LimitOffsetPagination` on a per-view basis.
+
+#### Configuration
+
+The `LimitOffsetPagination` class includes a number of attributes that may be overridden to modify the pagination style.
+
+To set these attributes you should override the `LimitOffsetPagination` class, and then enable your custom pagination class as above.
+
+* `default_limit` - A numeric value indicating the limit to use if one is not provided by the client in a query parameter. Defaults to the same value as the `DEFAULT_PAGE_SIZE` settings key.
+* `limit_query_param` - A string value indicating the name of the "limit" query parameter. Defaults to `'limit'`.
+* `offset_query_param` - A string value indicating the name of the "offset" query parameter. Defaults to `'offset'`.
+* `max_limit` - If set this is a numeric value indicating the maximum allowable limit that may be requested by the client. Defaults to `None`.
+* `template` - The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/numbers.html"`.
+
+---
## CursorPagination
-**TODO**
+The cursor-based pagination presents an opaque "cursor" indicator that the client may use to page through the result set. This pagination style only presents forward and reverse controls, and does not allow the client to navigate to arbitrary positions.
+
+Cursor based pagination requires that there is a unique, unchanging ordering of items in the result set. This ordering might typically be a creation timestamp on the records, as this presents a consistent ordering to paginate against.
+
+Cursor based pagination is more complex than other schemes. It also requires that the result set presents a fixed ordering, and does not allow the client to arbitrarily index into the result set. However it does provide the following benefits:
+
+* Provides a consistent pagination view. When used properly `CursorPagination` ensures that the client will never see the same item twice when paging through records.
+* Supports usage with very large datasets. With extremely large datasets pagination using offset-based pagination styles may become inefficient or unusable. Cursor based pagination schemes instead have fixed-time properties, and do not slow down as the dataset size increases.
+
+#### Details and limitations
+
+This implementation of cursor pagination uses a smart "position plus offset" style that allows it to properly support not-strictly-unique values as the ordering.
+
+It should be noted that using non-unique values the ordering does introduce the possibility of paging artifacts, where pagination consistency is no longer 100% guaranteed.
+
+**TODO**: Notes on `None`.
+
+The implementation also supports both forward and reverse pagination, which is often not supported in other implementations.
+
+For more technical details on the implementation we use for cursor pagination, the ["Building cursors for the Disqus API"][disqus-cursor-api] blog post gives a good overview of the basic approach.
+
+#### Setup
+
+To enable the `CursorPagination` style globally, use the following configuration, modifying the `DEFAULT_PAGE_SIZE` as desired:
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination',
+ 'DEFAULT_PAGE_SIZE': 100
+ }
+
+On `GenericAPIView` subclasses you may also set the `pagination_class` attribute to select `CursorPagination` on a per-view basis.
+
+#### Configuration
+
+The `CursorPagination` class includes a number of attributes that may be overridden to modify the pagination style.
+
+To set these attributes you should override the `CursorPagination` class, and then enable your custom pagination class as above.
+
+* `page_size` = A numeric value indicating the page size. If set, this overrides the `DEFAULT_PAGE_SIZE` setting. Defaults to the same value as the `DEFAULT_PAGE_SIZE` settings key.
+* `cursor_query_param` = A string value indicating the name of the "cursor" query parameter. Defaults to `'cursor'`.
+* `ordering` = This should be a string, or list of strings, indicating the field against which the cursor based pagination will be applied. For example: `ordering = 'created'`. Any filters on the view which define a `get_ordering` will override this attribute. Defaults to `None`.
+* `template` = The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/previous_and_next.html"`.
---
@@ -108,7 +234,7 @@ To have your custom pagination class be used by default, use the `DEFAULT_PAGINA
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'my_project.apps.core.pagination.LinkHeaderPagination',
- 'PAGINATE_BY': 10
+ 'DEFAULT_PAGE_SIZE': 10
}
API responses for list endpoints will now include a `Link` header, instead of including the pagination links as part of the body of the response, for example:
@@ -123,8 +249,25 @@ API responses for list endpoints will now include a `Link` header, instead of in
# HTML pagination controls
+By default using the pagination classes will cause HTML pagination controls to be displayed in the browsable API. There are two built-in display styles. The `PageNumberPagination` and `LimitOffsetPagination` classes display a list of page numbers with previous and next controls. The `CursorPagination` class displays a simpler style that only displays a previous and next control.
+
## Customizing the controls
+You can override the templates that render the HTML pagination controls. The two built-in styles are:
+
+* `rest_framework/pagination/numbers.html`
+* `rest_framework/pagination/previous_and_next.html`
+
+Providing a template with either of these paths in a global template directory will override the default rendering for the relevant pagination classes.
+
+Alternatively you can disable HTML pagination controls completely by subclassing on of the existing classes, setting `template = None` as an attribute on the class. You'll then need to configure your `DEFAULT_PAGINATION_CLASS` settings key to use your custom class as the default pagination style.
+
+#### Low-level API
+
+The low-level API for determining if a pagination class should display the controls or not is exposed as a `display_page_controls` attribute on the pagination instance. Custom pagination classes should be set to `True` in the `paginate_queryset` method if they require the HTML pagination controls to be displayed.
+
+The `.to_html()` and `.get_html_context()` methods may also be overridden in a custom pagination class in order to further customize how the controls are rendered.
+
---
# Third party packages
@@ -140,3 +283,4 @@ The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin`
[link-header]: ../img/link-header-pagination.png
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
+[disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/ \ No newline at end of file
diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md
index d9a1696d..9dc3f2bf 100644
--- a/docs/api-guide/testing.md
+++ b/docs/api-guide/testing.md
@@ -115,7 +115,7 @@ Extends [Django's existing `Client` class][client].
## Making requests
-The `APIClient` class supports the same request interface as `APIRequestFactory`. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example:
+The `APIClient` class supports the same request interface as Django's standard `Client` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example:
from rest_framework.test import APIClient
@@ -269,6 +269,6 @@ For example, to add support for using `format='html'` in test requests, you migh
}
[cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper
-[client]: https://docs.djangoproject.com/en/dev/topics/testing/overview/#module-django.test.client
+[client]: https://docs.djangoproject.com/en/dev/topics/testing/tools/#the-test-client
[requestfactory]: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.client.RequestFactory
[configuration]: #configuration
diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md
index e26e3a2f..2f46e1fc 100644
--- a/docs/topics/third-party-resources.md
+++ b/docs/topics/third-party-resources.md
@@ -188,6 +188,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [hawkrest][hawkrest] - Provides Hawk HTTP Authorization.
* [djangorestframework-httpsignature][djangorestframework-httpsignature] - Provides an easy to use HTTP Signature Authentication mechanism.
* [djoser][djoser] - Provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation.
+* [django-rest-auth][django-rest-auth] - Provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc.
### Permissions
@@ -324,3 +325,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[django-rest-framework-and-angularjs-video]: http://www.youtube.com/watch?v=q8frbgtj020
[web-api-performance-profiling-django-rest-framework]: http://dabapps.com/blog/api-performance-profiling-django-rest-framework/
[api-development-with-django-and-django-rest-framework]: https://bnotions.com/api-development-with-django-and-django-rest-framework/
+[django-rest-auth]: https://github.com/Tivix/django-rest-auth/
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index a5348922..13ea6dde 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -556,15 +556,15 @@ class CharField(Field):
def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
self.trim_whitespace = kwargs.pop('trim_whitespace', True)
- max_length = kwargs.pop('max_length', None)
- min_length = kwargs.pop('min_length', None)
+ self.max_length = kwargs.pop('max_length', None)
+ self.min_length = kwargs.pop('min_length', None)
super(CharField, self).__init__(**kwargs)
- if max_length is not None:
- message = self.error_messages['max_length'].format(max_length=max_length)
- self.validators.append(MaxLengthValidator(max_length, message=message))
- if min_length is not None:
- message = self.error_messages['min_length'].format(min_length=min_length)
- self.validators.append(MinLengthValidator(min_length, message=message))
+ if self.max_length is not None:
+ message = self.error_messages['max_length'].format(max_length=self.max_length)
+ self.validators.append(MaxLengthValidator(self.max_length, message=message))
+ if self.min_length is not None:
+ message = self.error_messages['min_length'].format(min_length=self.min_length)
+ self.validators.append(MinLengthValidator(self.min_length, message=message))
def run_validation(self, data=empty):
# Test for the empty string here so that it does not get validated,
@@ -658,15 +658,15 @@ class IntegerField(Field):
MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
def __init__(self, **kwargs):
- max_value = kwargs.pop('max_value', None)
- min_value = kwargs.pop('min_value', None)
+ self.max_value = kwargs.pop('max_value', None)
+ self.min_value = kwargs.pop('min_value', None)
super(IntegerField, self).__init__(**kwargs)
- if max_value is not None:
- message = self.error_messages['max_value'].format(max_value=max_value)
- self.validators.append(MaxValueValidator(max_value, message=message))
- if min_value is not None:
- message = self.error_messages['min_value'].format(min_value=min_value)
- self.validators.append(MinValueValidator(min_value, message=message))
+ if self.max_value is not None:
+ message = self.error_messages['max_value'].format(max_value=self.max_value)
+ self.validators.append(MaxValueValidator(self.max_value, message=message))
+ if self.min_value is not None:
+ message = self.error_messages['min_value'].format(min_value=self.min_value)
+ self.validators.append(MinValueValidator(self.min_value, message=message))
def to_internal_value(self, data):
if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH:
@@ -692,15 +692,15 @@ class FloatField(Field):
MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
def __init__(self, **kwargs):
- max_value = kwargs.pop('max_value', None)
- min_value = kwargs.pop('min_value', None)
+ self.max_value = kwargs.pop('max_value', None)
+ self.min_value = kwargs.pop('min_value', None)
super(FloatField, self).__init__(**kwargs)
- if max_value is not None:
- message = self.error_messages['max_value'].format(max_value=max_value)
- self.validators.append(MaxValueValidator(max_value, message=message))
- if min_value is not None:
- message = self.error_messages['min_value'].format(min_value=min_value)
- self.validators.append(MinValueValidator(min_value, message=message))
+ if self.max_value is not None:
+ message = self.error_messages['max_value'].format(max_value=self.max_value)
+ self.validators.append(MaxValueValidator(self.max_value, message=message))
+ if self.min_value is not None:
+ message = self.error_messages['min_value'].format(min_value=self.min_value)
+ self.validators.append(MinValueValidator(self.min_value, message=message))
def to_internal_value(self, data):
if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH:
@@ -733,13 +733,18 @@ class DecimalField(Field):
self.max_digits = max_digits
self.decimal_places = decimal_places
self.coerce_to_string = coerce_to_string if (coerce_to_string is not None) else self.coerce_to_string
+
+ self.max_value = max_value
+ self.min_value = min_value
+
super(DecimalField, self).__init__(**kwargs)
- if max_value is not None:
- message = self.error_messages['max_value'].format(max_value=max_value)
- self.validators.append(MaxValueValidator(max_value, message=message))
- if min_value is not None:
- message = self.error_messages['min_value'].format(min_value=min_value)
- self.validators.append(MinValueValidator(min_value, message=message))
+
+ if self.max_value is not None:
+ message = self.error_messages['max_value'].format(max_value=self.max_value)
+ self.validators.append(MaxValueValidator(self.max_value, message=message))
+ if self.min_value is not None:
+ message = self.error_messages['min_value'].format(min_value=self.min_value)
+ self.validators.append(MinValueValidator(self.min_value, message=message))
def to_internal_value(self, data):
"""
diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py
index 3b058fab..bf3611aa 100644
--- a/rest_framework/metadata.py
+++ b/rest_framework/metadata.py
@@ -115,7 +115,13 @@ class SimpleMetadata(BaseMetadata):
field_info['type'] = self.label_lookup[field]
field_info['required'] = getattr(field, 'required', False)
- for attr in ['read_only', 'label', 'help_text', 'min_length', 'max_length']:
+ attrs = [
+ 'read_only', 'label', 'help_text',
+ 'min_length', 'max_length',
+ 'min_value', 'max_value'
+ ]
+
+ for attr in attrs:
value = getattr(field, attr, None)
if value is not None and value != '':
field_info[attr] = force_text(value, strings_only=True)
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index 496500ba..80985873 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -259,7 +259,7 @@ class PageNumberPagination(BasePagination):
)
raise NotFound(msg)
- if paginator.count > 1:
+ if paginator.count > 1 and self.template is not None:
# The browsable API should display pagination controls.
self.display_page_controls = True
@@ -347,7 +347,7 @@ class LimitOffsetPagination(BasePagination):
self.offset = self.get_offset(request)
self.count = _get_count(queryset)
self.request = request
- if self.count > self.limit:
+ if self.count > self.limit and self.template is not None:
self.display_page_controls = True
return queryset[self.offset:self.offset + self.limit]
@@ -518,7 +518,7 @@ class CursorPagination(BasePagination):
# Display page controls in the browsable API if there is more
# than one page.
- if self.has_previous or self.has_next:
+ if (self.has_previous or self.has_next) and self.template is not None:
self.display_page_controls = True
return self.page
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index 6a4184e2..b1e39ff7 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -165,34 +165,30 @@ class SimpleRouter(BaseRouter):
else:
list_routes.append((httpmethods, methodname))
+ def _get_dynamic_routes(route, dynamic_routes):
+ ret = []
+ for httpmethods, methodname in dynamic_routes:
+ method_kwargs = getattr(viewset, methodname).kwargs
+ initkwargs = route.initkwargs.copy()
+ initkwargs.update(method_kwargs)
+ url_path = initkwargs.pop("url_path", None) or methodname
+ ret.append(Route(
+ url=replace_methodname(route.url, url_path),
+ mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
+ name=replace_methodname(route.name, url_path),
+ initkwargs=initkwargs,
+ ))
+
+ return ret
+
ret = []
for route in self.routes:
if isinstance(route, DynamicDetailRoute):
# Dynamic detail routes (@detail_route decorator)
- for httpmethods, methodname in detail_routes:
- method_kwargs = getattr(viewset, methodname).kwargs
- url_path = method_kwargs.pop("url_path", None) or methodname
- initkwargs = route.initkwargs.copy()
- initkwargs.update(method_kwargs)
- ret.append(Route(
- url=replace_methodname(route.url, url_path),
- mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
- name=replace_methodname(route.name, url_path),
- initkwargs=initkwargs,
- ))
+ ret += _get_dynamic_routes(route, detail_routes)
elif isinstance(route, DynamicListRoute):
# Dynamic list routes (@list_route decorator)
- for httpmethods, methodname in list_routes:
- method_kwargs = getattr(viewset, methodname).kwargs
- url_path = method_kwargs.pop("url_path", None) or methodname
- initkwargs = route.initkwargs.copy()
- initkwargs.update(method_kwargs)
- ret.append(Route(
- url=replace_methodname(route.url, url_path),
- mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
- name=replace_methodname(route.name, url_path),
- initkwargs=initkwargs,
- ))
+ ret += _get_dynamic_routes(route, list_routes)
else:
# Standard route
ret.append(route)
diff --git a/rest_framework/test.py b/rest_framework/test.py
index 4f4b7c20..a83d082a 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -209,7 +209,8 @@ class APIClient(APIRequestFactory, DjangoClient):
self.handler._force_user = None
self.handler._force_token = None
- return super(APIClient, self).logout()
+ if self.session:
+ super(APIClient, self).logout()
class APITransactionTestCase(testcases.TransactionTestCase):
diff --git a/tests/test_metadata.py b/tests/test_metadata.py
index 5031c0f3..3a435f02 100644
--- a/tests/test_metadata.py
+++ b/tests/test_metadata.py
@@ -54,8 +54,12 @@ class TestMetadata:
"""
class ExampleSerializer(serializers.Serializer):
choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
- integer_field = serializers.IntegerField(max_value=10)
- char_field = serializers.CharField(required=False)
+ integer_field = serializers.IntegerField(
+ min_value=1, max_value=1000
+ )
+ char_field = serializers.CharField(
+ required=False, min_length=3, max_length=40
+ )
class ExampleView(views.APIView):
"""Example view."""
@@ -96,13 +100,18 @@ class TestMetadata:
'type': 'integer',
'required': True,
'read_only': False,
- 'label': 'Integer field'
+ 'label': 'Integer field',
+ 'min_value': 1,
+ 'max_value': 1000,
+
},
'char_field': {
'type': 'string',
'required': False,
'read_only': False,
- 'label': 'Char field'
+ 'label': 'Char field',
+ 'min_length': 3,
+ 'max_length': 40
}
}
}
diff --git a/tests/test_routers.py b/tests/test_routers.py
index 948c69bb..08c58ec7 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -302,12 +302,16 @@ class DynamicListAndDetailViewSet(viewsets.ViewSet):
return Response({'method': 'link2'})
+class SubDynamicListAndDetailViewSet(DynamicListAndDetailViewSet):
+ pass
+
+
class TestDynamicListAndDetailRouter(TestCase):
def setUp(self):
self.router = SimpleRouter()
- def test_list_and_detail_route_decorators(self):
- routes = self.router.get_routes(DynamicListAndDetailViewSet)
+ def _test_list_and_detail_route_decorators(self, viewset):
+ routes = self.router.get_routes(viewset)
decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))]
MethodNamesMap = namedtuple('MethodNamesMap', 'method_name url_path')
@@ -336,3 +340,9 @@ class TestDynamicListAndDetailRouter(TestCase):
else:
method_map = 'get'
self.assertEqual(route.mapping[method_map], method_name)
+
+ def test_list_and_detail_route_decorators(self):
+ self._test_list_and_detail_route_decorators(DynamicListAndDetailViewSet)
+
+ def test_inherited_list_and_detail_route_decorators(self):
+ self._test_list_and_detail_route_decorators(SubDynamicListAndDetailViewSet)