Commit 19f9adac by Tom Christie

Merge branch 'master' into display-raw-data

parents e4d2f545 02b6836e
...@@ -73,7 +73,7 @@ The following attributes control the basic view behavior. ...@@ -73,7 +73,7 @@ The following attributes control the basic view behavior.
**Pagination**: **Pagination**:
The following attibutes are used to control pagination when used with list views. The following attributes are used to control pagination when used with list views.
* `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`. * `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`.
* `paginate_by_param` - The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`. * `paginate_by_param` - The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`.
...@@ -135,7 +135,7 @@ For example: ...@@ -135,7 +135,7 @@ For example:
#### `get_paginate_by(self)` #### `get_paginate_by(self)`
Returns the page size to use with pagination. By default this uses the `paginate_by` attribute, and may be overridden by the cient if the `paginate_by_param` attribute is set. Returns the page size to use with pagination. By default this uses the `paginate_by` attribute, and may be overridden by the client if the `paginate_by_param` attribute is set.
You may want to override this method to provide more complex behavior such as modifying page sizes based on the media type of the response. You may want to override this method to provide more complex behavior such as modifying page sizes based on the media type of the response.
......
...@@ -85,11 +85,12 @@ We could now use our pagination serializer in a view like this. ...@@ -85,11 +85,12 @@ We could now use our pagination serializer in a view like this.
The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default. You can customise this behaviour by altering the pagination style, by modifying the default number of results, by allowing clients to override the page size using a query parameter, or by turning pagination off completely. The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default. You can customise this behaviour by altering the pagination style, by modifying the default number of results, by allowing clients to override the page size using a query parameter, or by turning pagination off completely.
The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY` and `PAGINATE_BY_PARAM` settings. For example. The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY`, `PAGINATE_BY_PARAM`, and `MAX_PAGINATE_BY` settings. For example.
REST_FRAMEWORK = { REST_FRAMEWORK = {
'PAGINATE_BY': 10, 'PAGINATE_BY': 10, # Default to 10
'PAGINATE_BY_PARAM': 'page_size' 'PAGINATE_BY_PARAM': 'page_size', # Allow client to override, using `?page_size=xxx`.
'MAX_PAGINATE_BY': 100 # Maximum limit allowed when using `?page_size=xxx`.
} }
You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view. You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view.
...@@ -99,6 +100,7 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie ...@@ -99,6 +100,7 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie
serializer_class = ExampleModelSerializer serializer_class = ExampleModelSerializer
paginate_by = 10 paginate_by = 10
paginate_by_param = 'page_size' paginate_by_param = 'page_size'
max_paginate_by = 100
Note that using a `paginate_by` value of `None` will turn off pagination for the view. Note that using a `paginate_by` value of `None` will turn off pagination for the view.
......
...@@ -212,6 +212,10 @@ The following third party packages are also available. ...@@ -212,6 +212,10 @@ The following third party packages are also available.
The [DRF Any Permissions][drf-any-permissions] packages provides a different permission behavior in contrast to REST framework. Instead of all specified permissions being required, only one of the given permissions has to be true in order to get access to the view. The [DRF Any Permissions][drf-any-permissions] packages provides a different permission behavior in contrast to REST framework. Instead of all specified permissions being required, only one of the given permissions has to be true in order to get access to the view.
## Composed Permissions
The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components.
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
[authentication]: authentication.md [authentication]: authentication.md
[throttling]: throttling.md [throttling]: throttling.md
...@@ -222,3 +226,4 @@ The [DRF Any Permissions][drf-any-permissions] packages provides a different per ...@@ -222,3 +226,4 @@ The [DRF Any Permissions][drf-any-permissions] packages provides a different per
[2.2-announcement]: ../topics/2.2-announcement.md [2.2-announcement]: ../topics/2.2-announcement.md
[filtering]: filtering.md [filtering]: filtering.md
[drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions [drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions
[composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions
...@@ -117,7 +117,7 @@ For more information see the [browser enhancements documentation]. ...@@ -117,7 +117,7 @@ For more information see the [browser enhancements documentation].
# Standard HttpRequest attributes # Standard HttpRequest attributes
As REST framework's `Request` extends Django's `HttpRequest`, all the other standard attributes and methods are also available. For example the `request.META` dictionary is available as normal. As REST framework's `Request` extends Django's `HttpRequest`, all the other standard attributes and methods are also available. For example the `request.META` and `request.session` dictionaries are available as normal.
Note that due to implementation reasons the `Request` class does not inherit from `HttpRequest` class, but instead extends the class using composition. Note that due to implementation reasons the `Request` class does not inherit from `HttpRequest` class, but instead extends the class using composition.
......
...@@ -127,6 +127,35 @@ Default: `None` ...@@ -127,6 +127,35 @@ Default: `None`
The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If set to `None`, clients may not override the default page size. The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If set to `None`, clients may not override the default page size.
For example, given the following settings:
REST_FRAMEWORK = {
'PAGINATE_BY': 10,
'PAGINATE_BY_PARAM': 'page_size',
}
A client would be able to modify the pagination size by using the `page_size` query parameter. For example:
GET http://example.com/api/accounts?page_size=25
Default: `None`
#### MAX_PAGINATE_BY
The maximum page size to allow when the page size is specified by the client. If set to `None`, then no maximum limit is applied.
For example, given the following settings:
REST_FRAMEWORK = {
'PAGINATE_BY': 10,
'PAGINATE_BY_PARAM': 'page_size',
'MAX_PAGINATE_BY': 100
}
A client request like the following would return a paginated list of up to 100 items.
GET http://example.com/api/accounts?page_size=999
Default: `None` Default: `None`
--- ---
......
...@@ -70,6 +70,13 @@ Or, if you're using the `@api_view` decorator with function based views. ...@@ -70,6 +70,13 @@ Or, if you're using the `@api_view` decorator with function based views.
The throttle classes provided by REST framework use Django's cache backend. You should make sure that you've set appropriate [cache settings][cache-setting]. The default value of `LocMemCache` backend should be okay for simple setups. See Django's [cache documentation][cache-docs] for more details. The throttle classes provided by REST framework use Django's cache backend. You should make sure that you've set appropriate [cache settings][cache-setting]. The default value of `LocMemCache` backend should be okay for simple setups. See Django's [cache documentation][cache-docs] for more details.
If you need to use a cache other than `'default'`, you can do so by creating a custom throttle class and setting the `cache` attribute. For example:
class CustomAnonRateThrottle(AnonRateThrottle):
cache = get_cache('alternate')
You'll need to rememeber to also set your custom throttle class in the `'DEFAULT_THROTTLE_CLASSES'` settings key, or using the `throttle_classes` view attribute.
--- ---
# API Reference # API Reference
......
...@@ -200,7 +200,7 @@ To run the tests against all supported configurations, first install [the tox te ...@@ -200,7 +200,7 @@ To run the tests against all supported configurations, first install [the tox te
## Support ## Support
For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag. For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, search [the IRC archives][botbot], or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag.
[Paid support is available][paid-support] from [DabApps][dabapps], and can include work on REST framework core, or support with building your REST framework API. Please [contact DabApps][contact-dabapps] if you'd like to discuss commercial support options. [Paid support is available][paid-support] from [DabApps][dabapps], and can include work on REST framework core, or support with building your REST framework API. Please [contact DabApps][contact-dabapps] if you'd like to discuss commercial support options.
...@@ -307,6 +307,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -307,6 +307,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[tox]: http://testrun.org/tox/latest/ [tox]: http://testrun.org/tox/latest/
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[botbot]: https://botbot.me/freenode/restframework/
[stack-overflow]: http://stackoverflow.com/ [stack-overflow]: http://stackoverflow.com/
[django-rest-framework-tag]: http://stackoverflow.com/questions/tagged/django-rest-framework [django-rest-framework-tag]: http://stackoverflow.com/questions/tagged/django-rest-framework
[django-tag]: http://stackoverflow.com/questions/tagged/django [django-tag]: http://stackoverflow.com/questions/tagged/django
......
...@@ -163,6 +163,9 @@ The following people have helped make REST framework great. ...@@ -163,6 +163,9 @@ The following people have helped make REST framework great.
* Krzysztof Jurewicz - [krzysiekj] * Krzysztof Jurewicz - [krzysiekj]
* Eric Buehl - [ericbuehl] * Eric Buehl - [ericbuehl]
* Kristian Øllegaard - [kristianoellegaard] * Kristian Øllegaard - [kristianoellegaard]
* Alexander Akhmetov - [alexander-akhmetov]
* Andrey Antukh - [niwibe]
* Mathieu Pillard - [diox]
Many thanks to everyone who's contributed to the project. Many thanks to everyone who's contributed to the project.
...@@ -362,3 +365,6 @@ You can also contact [@_tomchristie][twitter] directly on twitter. ...@@ -362,3 +365,6 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[krzysiekj]: https://github.com/krzysiekj [krzysiekj]: https://github.com/krzysiekj
[ericbuehl]: https://github.com/ericbuehl [ericbuehl]: https://github.com/ericbuehl
[kristianoellegaard]: https://github.com/kristianoellegaard [kristianoellegaard]: https://github.com/kristianoellegaard
[alexander-akhmetov]: https://github.com/alexander-akhmetov
[niwibe]: https://github.com/niwibe
[diox]: https://github.com/diox
...@@ -43,9 +43,12 @@ You can determine your currently installed version using `pip freeze`: ...@@ -43,9 +43,12 @@ You can determine your currently installed version using `pip freeze`:
### Master ### Master
* Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. * Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings.
* Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute.
* Added `cache` attribute to throttles to allow overriding of default cache.
* Bugfix: `required=True` argument fixed for boolean serializer fields. * Bugfix: `required=True` argument fixed for boolean serializer fields.
* Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists.
* Bugfix: Client sending emptry string instead of file now clears `FileField`. * Bugfix: Client sending emptry string instead of file now clears `FileField`.
* Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`.
### 2.3.7 ### 2.3.7
......
...@@ -514,6 +514,11 @@ class ChoiceField(WritableField): ...@@ -514,6 +514,11 @@ class ChoiceField(WritableField):
return True return True
return False return False
def from_native(self, value):
if value in validators.EMPTY_VALUES:
return None
return super(ChoiceField, self).from_native(value)
class EmailField(CharField): class EmailField(CharField):
type_name = 'EmailField' type_name = 'EmailField'
......
...@@ -14,13 +14,15 @@ from rest_framework.settings import api_settings ...@@ -14,13 +14,15 @@ from rest_framework.settings import api_settings
import warnings import warnings
def strict_positive_int(integer_string): def strict_positive_int(integer_string, cutoff=None):
""" """
Cast a string to a strictly positive integer. Cast a string to a strictly positive integer.
""" """
ret = int(integer_string) ret = int(integer_string)
if ret <= 0: if ret <= 0:
raise ValueError() raise ValueError()
if cutoff:
ret = min(ret, cutoff)
return ret return ret
def get_object_or_404(queryset, **filter_kwargs): def get_object_or_404(queryset, **filter_kwargs):
...@@ -56,6 +58,7 @@ class GenericAPIView(views.APIView): ...@@ -56,6 +58,7 @@ class GenericAPIView(views.APIView):
# Pagination settings # Pagination settings
paginate_by = api_settings.PAGINATE_BY paginate_by = api_settings.PAGINATE_BY
paginate_by_param = api_settings.PAGINATE_BY_PARAM paginate_by_param = api_settings.PAGINATE_BY_PARAM
max_paginate_by = api_settings.MAX_PAGINATE_BY
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
page_kwarg = 'page' page_kwarg = 'page'
...@@ -205,9 +208,11 @@ class GenericAPIView(views.APIView): ...@@ -205,9 +208,11 @@ class GenericAPIView(views.APIView):
PendingDeprecationWarning, stacklevel=2) PendingDeprecationWarning, stacklevel=2)
if self.paginate_by_param: if self.paginate_by_param:
query_params = self.request.QUERY_PARAMS
try: try:
return int(query_params[self.paginate_by_param]) return strict_positive_int(
self.request.QUERY_PARAMS[self.paginate_by_param],
cutoff=self.max_paginate_by
)
except (KeyError, ValueError): except (KeyError, ValueError):
pass pass
......
...@@ -48,7 +48,6 @@ DEFAULTS = { ...@@ -48,7 +48,6 @@ DEFAULTS = {
), ),
'DEFAULT_THROTTLE_CLASSES': ( 'DEFAULT_THROTTLE_CLASSES': (
), ),
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'DEFAULT_CONTENT_NEGOTIATION_CLASS':
'rest_framework.negotiation.DefaultContentNegotiation', 'rest_framework.negotiation.DefaultContentNegotiation',
...@@ -68,15 +67,16 @@ DEFAULTS = { ...@@ -68,15 +67,16 @@ DEFAULTS = {
# Pagination # Pagination
'PAGINATE_BY': None, 'PAGINATE_BY': None,
'PAGINATE_BY_PARAM': None, 'PAGINATE_BY_PARAM': None,
'MAX_PAGINATE_BY': None,
# View configuration
'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name',
'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description',
# Authentication # Authentication
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None, 'UNAUTHENTICATED_TOKEN': None,
# View configuration
'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name',
'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description',
# Testing # Testing
'TEST_REQUEST_RENDERER_CLASSES': ( 'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework.renderers.MultiPartRenderer', 'rest_framework.renderers.MultiPartRenderer',
......
...@@ -688,6 +688,14 @@ class ChoiceFieldTests(TestCase): ...@@ -688,6 +688,14 @@ class ChoiceFieldTests(TestCase):
f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES) f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES)
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES) self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES)
def test_from_native_empty(self):
"""
Make sure from_native() returns None on empty param.
"""
f = serializers.ChoiceField(choices=self.SAMPLE_CHOICES)
result = f.from_native('')
self.assertEqual(result, None)
class EmailFieldTests(TestCase): class EmailFieldTests(TestCase):
""" """
......
...@@ -42,6 +42,16 @@ class PaginateByParamView(generics.ListAPIView): ...@@ -42,6 +42,16 @@ class PaginateByParamView(generics.ListAPIView):
paginate_by_param = 'page_size' paginate_by_param = 'page_size'
class MaxPaginateByView(generics.ListAPIView):
"""
View for testing custom max_paginate_by usage
"""
model = BasicModel
paginate_by = 3
max_paginate_by = 5
paginate_by_param = 'page_size'
class IntegrationTestPagination(TestCase): class IntegrationTestPagination(TestCase):
""" """
Integration tests for paginated list views. Integration tests for paginated list views.
...@@ -313,6 +323,43 @@ class TestCustomPaginateByParam(TestCase): ...@@ -313,6 +323,43 @@ class TestCustomPaginateByParam(TestCase):
self.assertEqual(response.data['results'], self.data[:5]) self.assertEqual(response.data['results'], self.data[:5])
class TestMaxPaginateByParam(TestCase):
"""
Tests for list views with max_paginate_by kwarg
"""
def setUp(self):
"""
Create 13 BasicModel instances.
"""
for i in range(13):
BasicModel(text=i).save()
self.objects = BasicModel.objects
self.data = [
{'id': obj.id, 'text': obj.text}
for obj in self.objects.all()
]
self.view = MaxPaginateByView.as_view()
def test_max_paginate_by(self):
"""
If max_paginate_by is set, it should limit page size for the view.
"""
request = factory.get('/?page_size=10')
response = self.view(request).render()
self.assertEqual(response.data['count'], 13)
self.assertEqual(response.data['results'], self.data[:5])
def test_max_paginate_by_without_page_size_param(self):
"""
If max_paginate_by is set, but client does not specifiy page_size,
standard `paginate_by` behavior should be used.
"""
request = factory.get('/')
response = self.view(request).render()
self.assertEqual(response.data['results'], self.data[:3])
### Tests for context in pagination serializers ### Tests for context in pagination serializers
class CustomField(serializers.Field): class CustomField(serializers.Field):
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Provides various throttling policies. Provides various throttling policies.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.cache import cache from django.core.cache import cache as default_cache
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
import time import time
...@@ -39,6 +39,7 @@ class SimpleRateThrottle(BaseThrottle): ...@@ -39,6 +39,7 @@ class SimpleRateThrottle(BaseThrottle):
Previous request information used for throttling is stored in the cache. Previous request information used for throttling is stored in the cache.
""" """
cache = default_cache
timer = time.time timer = time.time
cache_format = 'throtte_%(scope)s_%(ident)s' cache_format = 'throtte_%(scope)s_%(ident)s'
scope = None scope = None
...@@ -99,7 +100,7 @@ class SimpleRateThrottle(BaseThrottle): ...@@ -99,7 +100,7 @@ class SimpleRateThrottle(BaseThrottle):
if self.key is None: if self.key is None:
return True return True
self.history = cache.get(self.key, []) self.history = self.cache.get(self.key, [])
self.now = self.timer() self.now = self.timer()
# Drop any requests from the history which have now passed the # Drop any requests from the history which have now passed the
...@@ -116,7 +117,7 @@ class SimpleRateThrottle(BaseThrottle): ...@@ -116,7 +117,7 @@ class SimpleRateThrottle(BaseThrottle):
into the cache. into the cache.
""" """
self.history.insert(0, self.now) self.history.insert(0, self.now)
cache.set(self.key, self.history, self.duration) self.cache.set(self.key, self.history, self.duration)
return True return True
def throttle_failure(self): def throttle_failure(self):
...@@ -151,7 +152,9 @@ class AnonRateThrottle(SimpleRateThrottle): ...@@ -151,7 +152,9 @@ class AnonRateThrottle(SimpleRateThrottle):
if request.user.is_authenticated(): if request.user.is_authenticated():
return None # Only throttle unauthenticated requests. return None # Only throttle unauthenticated requests.
ident = request.META.get('REMOTE_ADDR', None) ident = request.META.get('HTTP_X_FORWARDED_FOR')
if ident is None:
ident = request.META.get('REMOTE_ADDR')
return self.cache_format % { return self.cache_format % {
'scope': self.scope, 'scope': self.scope,
......
...@@ -8,8 +8,11 @@ def get_breadcrumbs(url): ...@@ -8,8 +8,11 @@ def get_breadcrumbs(url):
tuple of (name, url). tuple of (name, url).
""" """
from rest_framework.settings import api_settings
from rest_framework.views import APIView from rest_framework.views import APIView
view_name_func = api_settings.VIEW_NAME_FUNCTION
def breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen): def breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen):
""" """
Add tuples of (name, url) to the breadcrumbs list, Add tuples of (name, url) to the breadcrumbs list,
...@@ -28,8 +31,8 @@ def get_breadcrumbs(url): ...@@ -28,8 +31,8 @@ def get_breadcrumbs(url):
# Don't list the same view twice in a row. # Don't list the same view twice in a row.
# Probably an optional trailing slash. # Probably an optional trailing slash.
if not seen or seen[-1] != view: if not seen or seen[-1] != view:
instance = view.cls() suffix = getattr(view, 'suffix', None)
name = instance.get_view_name() name = view_name_func(cls, suffix)
breadcrumbs_list.insert(0, (name, prefix + url)) breadcrumbs_list.insert(0, (name, prefix + url))
seen.append(view) seen.append(view)
......
...@@ -15,8 +15,14 @@ from rest_framework.settings import api_settings ...@@ -15,8 +15,14 @@ from rest_framework.settings import api_settings
from rest_framework.utils import formatting from rest_framework.utils import formatting
def get_view_name(cls, suffix=None): def get_view_name(view_cls, suffix=None):
name = cls.__name__ """
Given a view class, return a textual name to represent the view.
This name is used in the browsable API, and in OPTIONS responses.
This function is the default for the `VIEW_NAME_FUNCTION` setting.
"""
name = view_cls.__name__
name = formatting.remove_trailing_string(name, 'View') name = formatting.remove_trailing_string(name, 'View')
name = formatting.remove_trailing_string(name, 'ViewSet') name = formatting.remove_trailing_string(name, 'ViewSet')
name = formatting.camelcase_to_spaces(name) name = formatting.camelcase_to_spaces(name)
...@@ -25,17 +31,56 @@ def get_view_name(cls, suffix=None): ...@@ -25,17 +31,56 @@ def get_view_name(cls, suffix=None):
return name return name
def get_view_description(cls, html=False): def get_view_description(view_cls, html=False):
description = cls.__doc__ or '' """
Given a view class, return a textual description to represent the view.
This name is used in the browsable API, and in OPTIONS responses.
This function is the default for the `VIEW_DESCRIPTION_FUNCTION` setting.
"""
description = view_cls.__doc__ or ''
description = formatting.dedent(smart_text(description)) description = formatting.dedent(smart_text(description))
if html: if html:
return formatting.markup_description(description) return formatting.markup_description(description)
return description return description
def exception_handler(exc):
"""
Returns the response that should be used for any given exception.
By default we handle the REST framework `APIException`, and also
Django's builtin `Http404` and `PermissionDenied` exceptions.
Any unhandled exceptions may return `None`, which will cause a 500 error
to be raised.
"""
if isinstance(exc, exceptions.APIException):
headers = {}
if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None):
headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
return Response({'detail': exc.detail},
status=exc.status_code,
headers=headers)
elif isinstance(exc, Http404):
return Response({'detail': 'Not found'},
status=status.HTTP_404_NOT_FOUND)
elif isinstance(exc, PermissionDenied):
return Response({'detail': 'Permission denied'},
status=status.HTTP_403_FORBIDDEN)
# Note: Unhandled exceptions will raise a 500 error.
return None
class APIView(View): class APIView(View):
settings = api_settings
# The following policies may be set at either globally, or per-view.
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
...@@ -43,6 +88,9 @@ class APIView(View): ...@@ -43,6 +88,9 @@ class APIView(View):
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
# Allow dependancy injection of other settings to make testing easier.
settings = api_settings
@classmethod @classmethod
def as_view(cls, **initkwargs): def as_view(cls, **initkwargs):
""" """
...@@ -133,7 +181,7 @@ class APIView(View): ...@@ -133,7 +181,7 @@ class APIView(View):
Return the view name, as used in OPTIONS responses and in the Return the view name, as used in OPTIONS responses and in the
browsable API. browsable API.
""" """
func = api_settings.VIEW_NAME_FUNCTION func = self.settings.VIEW_NAME_FUNCTION
return func(self.__class__, getattr(self, 'suffix', None)) return func(self.__class__, getattr(self, 'suffix', None))
def get_view_description(self, html=False): def get_view_description(self, html=False):
...@@ -141,7 +189,7 @@ class APIView(View): ...@@ -141,7 +189,7 @@ class APIView(View):
Return some descriptive text for the view, as used in OPTIONS responses Return some descriptive text for the view, as used in OPTIONS responses
and in the browsable API. and in the browsable API.
""" """
func = api_settings.VIEW_DESCRIPTION_FUNCTION func = self.settings.VIEW_DESCRIPTION_FUNCTION
return func(self.__class__, html) return func(self.__class__, html)
# API policy instantiation methods # API policy instantiation methods
...@@ -303,34 +351,24 @@ class APIView(View): ...@@ -303,34 +351,24 @@ class APIView(View):
Handle any exception that occurs, by returning an appropriate response, Handle any exception that occurs, by returning an appropriate response,
or re-raising the error. or re-raising the error.
""" """
if isinstance(exc, exceptions.Throttled) and exc.wait is not None:
# Throttle wait header
self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
if isinstance(exc, (exceptions.NotAuthenticated, if isinstance(exc, (exceptions.NotAuthenticated,
exceptions.AuthenticationFailed)): exceptions.AuthenticationFailed)):
# WWW-Authenticate header for 401 responses, else coerce to 403 # WWW-Authenticate header for 401 responses, else coerce to 403
auth_header = self.get_authenticate_header(self.request) auth_header = self.get_authenticate_header(self.request)
if auth_header: if auth_header:
self.headers['WWW-Authenticate'] = auth_header exc.auth_header = auth_header
else: else:
exc.status_code = status.HTTP_403_FORBIDDEN exc.status_code = status.HTTP_403_FORBIDDEN
if isinstance(exc, exceptions.APIException): response = exception_handler(exc)
return Response({'detail': exc.detail},
status=exc.status_code, if response is None:
exception=True)
elif isinstance(exc, Http404):
return Response({'detail': 'Not found'},
status=status.HTTP_404_NOT_FOUND,
exception=True)
elif isinstance(exc, PermissionDenied):
return Response({'detail': 'Permission denied'},
status=status.HTTP_403_FORBIDDEN,
exception=True)
raise raise
response.exception = True
return response
# Note: session based authentication is explicitly CSRF validated, # Note: session based authentication is explicitly CSRF validated,
# all other authentication is CSRF exempt. # all other authentication is CSRF exempt.
@csrf_exempt @csrf_exempt
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment