From 316de3a8a314162e3d6ec081344eabca3a4d91b9 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Mon, 26 Aug 2013 20:05:36 +0400 Subject: [PATCH 01/20] Added max_paginate_by parameter --- rest_framework/generics.py | 10 ++++-- rest_framework/settings.py | 1 + rest_framework/tests/test_pagination.py | 46 +++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 5ecf6310d..33affee88 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -56,6 +56,7 @@ class GenericAPIView(views.APIView): # Pagination settings paginate_by = api_settings.PAGINATE_BY 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 page_kwarg = 'page' @@ -207,11 +208,16 @@ class GenericAPIView(views.APIView): if self.paginate_by_param: query_params = self.request.QUERY_PARAMS try: - return int(query_params[self.paginate_by_param]) + paginate_by_param = int(query_params[self.paginate_by_param]) except (KeyError, ValueError): pass + else: + if self.max_paginate_by: + return min(self.max_paginate_by, paginate_by_param) + else: + return paginate_by_param - return self.paginate_by + return min(self.max_paginate_by, self.paginate_by) or self.paginate_by def get_serializer_class(self): """ diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 7d25e5131..b8e40bfa5 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -68,6 +68,7 @@ DEFAULTS = { # Pagination 'PAGINATE_BY': None, 'PAGINATE_BY_PARAM': None, + 'MAX_PAGINATE_BY': None, # View configuration 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name', diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py index 85d4640ea..cbed16047 100644 --- a/rest_framework/tests/test_pagination.py +++ b/rest_framework/tests/test_pagination.py @@ -42,6 +42,16 @@ class PaginateByParamView(generics.ListAPIView): paginate_by_param = 'page_size' +class MaxPaginateByView(generics.ListAPIView): + """ + View for testing custom max_paginate_by usage + """ + model = BasicModel + paginate_by = 5 + max_paginate_by = 3 + paginate_by_param = 'page_size' + + class IntegrationTestPagination(TestCase): """ Integration tests for paginated list views. @@ -313,6 +323,42 @@ class TestCustomPaginateByParam(TestCase): 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 and it less than paginate_by, new kwarg should limit requests for review. + """ + request = factory.get('/?page_size=10') + response = self.view(request).render() + self.assertEqual(response.data['count'], 13) + self.assertEqual(response.data['results'], self.data[:3]) + + def test_max_paginate_by_without_page_size_param(self): + """ + If max_paginate_by is set, new kwarg should limit requests for review. + """ + request = factory.get('/') + response = self.view(request).render() + self.assertEqual(response.data['results'], self.data[:3]) + + ### Tests for context in pagination serializers class CustomField(serializers.Field): From dce47a11d3d65a697ea8aa322455d626190bc1e5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 27 Aug 2013 12:32:13 +0100 Subject: [PATCH 02/20] Move settings into more sensible ordering --- rest_framework/settings.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 7d25e5131..2ee15ac7c 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -48,7 +48,6 @@ DEFAULTS = { ), 'DEFAULT_THROTTLE_CLASSES': ( ), - 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation', @@ -69,14 +68,14 @@ DEFAULTS = { 'PAGINATE_BY': None, 'PAGINATE_BY_PARAM': None, - # View configuration - 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name', - 'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description', - # Authentication 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_TOKEN': None, + # View configuration + 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name', + 'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description', + # Testing 'TEST_REQUEST_RENDERER_CLASSES': ( 'rest_framework.renderers.MultiPartRenderer', From b430503fa657330b606a9c632ea0decc4254163e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 27 Aug 2013 12:32:33 +0100 Subject: [PATCH 03/20] Move exception handler out of main view --- rest_framework/views.py | 79 +++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 22 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 727a9f956..7cb71ccf8 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -15,8 +15,14 @@ from rest_framework.settings import api_settings from rest_framework.utils import formatting -def get_view_name(cls, suffix=None): - name = cls.__name__ +def get_view_name(view_cls, suffix=None): + """ + 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, 'ViewSet') name = formatting.camelcase_to_spaces(name) @@ -25,14 +31,53 @@ def get_view_name(cls, suffix=None): return name -def get_view_description(cls, html=False): - description = cls.__doc__ or '' +def get_view_description(view_cls, html=False): + """ + 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)) if html: return formatting.markup_description(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): settings = api_settings @@ -303,33 +348,23 @@ class APIView(View): Handle any exception that occurs, by returning an appropriate response, 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, exceptions.AuthenticationFailed)): # WWW-Authenticate header for 401 responses, else coerce to 403 auth_header = self.get_authenticate_header(self.request) if auth_header: - self.headers['WWW-Authenticate'] = auth_header + exc.auth_header = auth_header else: exc.status_code = status.HTTP_403_FORBIDDEN - if isinstance(exc, exceptions.APIException): - return Response({'detail': exc.detail}, - status=exc.status_code, - 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 + response = exception_handler(exc) + + if response is None: + raise + + response.exception = True + return response # Note: session based authentication is explicitly CSRF validated, # all other authentication is CSRF exempt. From b54cbd292c5680f4de0e028ff1cb2a9ab1cd34ff Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 27 Aug 2013 12:36:06 +0100 Subject: [PATCH 04/20] Use view.settings for API settings, to make testing easier. --- rest_framework/views.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 7cb71ccf8..4cff04224 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -79,8 +79,8 @@ def exception_handler(exc): 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 parser_classes = api_settings.DEFAULT_PARSER_CLASSES authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES @@ -88,6 +88,9 @@ class APIView(View): permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS + # Allow dependancy injection of other settings to make testing easier. + settings = api_settings + @classmethod def as_view(cls, **initkwargs): """ @@ -178,7 +181,7 @@ class APIView(View): Return the view name, as used in OPTIONS responses and in the browsable API. """ - func = api_settings.VIEW_NAME_FUNCTION + func = self.settings.VIEW_NAME_FUNCTION return func(self.__class__, getattr(self, 'suffix', None)) def get_view_description(self, html=False): @@ -186,7 +189,7 @@ class APIView(View): Return some descriptive text for the view, as used in OPTIONS responses and in the browsable API. """ - func = api_settings.VIEW_DESCRIPTION_FUNCTION + func = self.settings.VIEW_DESCRIPTION_FUNCTION return func(self.__class__, html) # API policy instantiation methods From ea6eee304c230a9277fdc76f4ac91654e0019b7a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 27 Aug 2013 12:37:55 +0100 Subject: [PATCH 05/20] Note 'request.session' as available on requests. --- docs/api-guide/requests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/requests.md b/docs/api-guide/requests.md index 39a34fcfb..0696fedf6 100644 --- a/docs/api-guide/requests.md +++ b/docs/api-guide/requests.md @@ -117,7 +117,7 @@ For more information see the [browser enhancements documentation]. # 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. From 7fb3f078f0973acc1d108d8c617b26b6845599f7 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Tue, 27 Aug 2013 17:38:41 +0400 Subject: [PATCH 06/20] fix for python3 --- rest_framework/generics.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 33affee88..ce6c462a3 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -212,12 +212,15 @@ class GenericAPIView(views.APIView): except (KeyError, ValueError): pass else: - if self.max_paginate_by: + if self.max_paginate_by is not None: return min(self.max_paginate_by, paginate_by_param) else: return paginate_by_param - return min(self.max_paginate_by, self.paginate_by) or self.paginate_by + if self.max_paginate_by: + return min(self.max_paginate_by, self.paginate_by) + else: + return self.paginate_by def get_serializer_class(self): """ From 4c53fb883fe719c3ca6244aeb8c405a24eb89a40 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 28 Aug 2013 12:52:38 +0100 Subject: [PATCH 07/20] Tweak MAX_PAGINATE_BY behavior in edge case. Always respect `paginate_by` settings if client does not specify page size. (Even if the developer has misconfigured, so that `paginate_by > max`.) --- rest_framework/generics.py | 20 ++++++++------------ rest_framework/tests/test_pagination.py | 11 ++++++----- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index ce6c462a3..14feed204 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -14,13 +14,15 @@ from rest_framework.settings import api_settings import warnings -def strict_positive_int(integer_string): +def strict_positive_int(integer_string, cutoff=None): """ Cast a string to a strictly positive integer. """ ret = int(integer_string) if ret <= 0: raise ValueError() + if cutoff: + ret = min(ret, cutoff) return ret def get_object_or_404(queryset, **filter_kwargs): @@ -206,21 +208,15 @@ class GenericAPIView(views.APIView): PendingDeprecationWarning, stacklevel=2) if self.paginate_by_param: - query_params = self.request.QUERY_PARAMS try: - paginate_by_param = 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): pass - else: - if self.max_paginate_by is not None: - return min(self.max_paginate_by, paginate_by_param) - else: - return paginate_by_param - if self.max_paginate_by: - return min(self.max_paginate_by, self.paginate_by) - else: - return self.paginate_by + return self.paginate_by def get_serializer_class(self): """ diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py index cbed16047..4170d4b64 100644 --- a/rest_framework/tests/test_pagination.py +++ b/rest_framework/tests/test_pagination.py @@ -47,8 +47,8 @@ class MaxPaginateByView(generics.ListAPIView): View for testing custom max_paginate_by usage """ model = BasicModel - paginate_by = 5 - max_paginate_by = 3 + paginate_by = 3 + max_paginate_by = 5 paginate_by_param = 'page_size' @@ -343,16 +343,17 @@ class TestMaxPaginateByParam(TestCase): def test_max_paginate_by(self): """ - If max_paginate_by is set and it less than paginate_by, new kwarg should limit requests for review. + 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[:3]) + self.assertEqual(response.data['results'], self.data[:5]) def test_max_paginate_by_without_page_size_param(self): """ - If max_paginate_by is set, new kwarg should limit requests for review. + 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() From 848567a0cd4f244bfe9fd68e97ae672bd259fd92 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 28 Aug 2013 12:55:49 +0100 Subject: [PATCH 08/20] Docs for `MAX_PAGINATE_BY` setting & view attribute. --- docs/api-guide/pagination.md | 8 +++++--- docs/api-guide/settings.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index ca0174b76..0829589f8 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -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 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 = { - 'PAGINATE_BY': 10, - 'PAGINATE_BY_PARAM': 'page_size' + 'PAGINATE_BY': 10, # Default to 10 + '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. @@ -99,6 +100,7 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie serializer_class = ExampleModelSerializer paginate_by = 10 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. diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index fe7925a5a..542e8c5fa 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -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. +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` --- From d7224afe5458f0b1016a80feec31c410c335dbce Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 28 Aug 2013 12:57:29 +0100 Subject: [PATCH 09/20] Added @alexander-akhmetov. For work on MAX_PAGINATE_BY, #1063. Thanks! :) --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 9b13131a6..49f06e785 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -163,6 +163,7 @@ The following people have helped make REST framework great. * Krzysztof Jurewicz - [krzysiekj] * Eric Buehl - [ericbuehl] * Kristian Øllegaard - [kristianoellegaard] +* Alexander Akhmetov - [alexander-akhmetov] Many thanks to everyone who's contributed to the project. @@ -362,3 +363,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [krzysiekj]: https://github.com/krzysiekj [ericbuehl]: https://github.com/ericbuehl [kristianoellegaard]: https://github.com/kristianoellegaard +[alexander-akhmetov]: htttps://github.com/alexander-akhmetov \ No newline at end of file From 97b52156cc0e96c2edb7e1b176838bfd9c22321a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 28 Aug 2013 13:34:14 +0100 Subject: [PATCH 10/20] Added `.cache` attribute on throttles. Closes #1066. More localised than a new settings key, and more flexible in that different throttles can use different behavior. Thanks to @chicheng for the report! :) --- docs/api-guide/throttling.md | 7 +++++++ rest_framework/throttling.py | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 42f9c228d..cc4692171 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -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. +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 diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 65b455930..8943f22c1 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -2,7 +2,7 @@ Provides various throttling policies. """ 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 rest_framework.settings import api_settings import time @@ -39,6 +39,7 @@ class SimpleRateThrottle(BaseThrottle): Previous request information used for throttling is stored in the cache. """ + cache = default_cache timer = time.time cache_format = 'throtte_%(scope)s_%(ident)s' scope = None @@ -99,7 +100,7 @@ class SimpleRateThrottle(BaseThrottle): if self.key is None: return True - self.history = cache.get(self.key, []) + self.history = self.cache.get(self.key, []) self.now = self.timer() # Drop any requests from the history which have now passed the @@ -116,7 +117,7 @@ class SimpleRateThrottle(BaseThrottle): into the cache. """ 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 def throttle_failure(self): From 711fb9761c9722a83c083257d15c0ec8f755ca7a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 28 Aug 2013 13:35:27 +0100 Subject: [PATCH 11/20] Update release notes. --- docs/topics/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 626831cbf..516efdc85 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -43,6 +43,8 @@ You can determine your currently installed version using `pip freeze`: ### Master * 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: `client.force_authenticate(None)` should also clear session info if it exists. * Bugfix: Client sending emptry string instead of file now clears `FileField`. From 2d5e14a8d39a53c8a2e6d28fb8ae7debb5fbd388 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 28 Aug 2013 15:32:41 +0100 Subject: [PATCH 12/20] Throttles now use HTTP_X_FORWARDED_FOR, falling back to REMOTE_ADDR to identify anonymous requests --- rest_framework/throttling.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 8943f22c1..a946d837f 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -152,7 +152,9 @@ class AnonRateThrottle(SimpleRateThrottle): if request.user.is_authenticated(): 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 % { 'scope': self.scope, From 2d37952e7872f7f69f588b02941ba6f5d739cdb6 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 29 Aug 2013 00:50:54 +0200 Subject: [PATCH 13/20] Add composed-permissions entry to the api-guide. --- docs/api-guide/permissions.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 12aa4c18b..a7bf15556 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -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. +## 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 [authentication]: authentication.md [throttling]: throttling.md @@ -222,3 +226,4 @@ The [DRF Any Permissions][drf-any-permissions] packages provides a different per [2.2-announcement]: ../topics/2.2-announcement.md [filtering]: filtering.md [drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions +[composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions From 6f8acb5a768d5d79efd7b39c5229bc4262e467a0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 09:31:12 +0100 Subject: [PATCH 14/20] Added @niwibe For docs addition #1070 - Thanks! --- docs/topics/credits.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 49f06e785..47807a0ed 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -164,6 +164,7 @@ The following people have helped make REST framework great. * Eric Buehl - [ericbuehl] * Kristian Øllegaard - [kristianoellegaard] * Alexander Akhmetov - [alexander-akhmetov] +* Andrey Antukh - [niwibe] Many thanks to everyone who's contributed to the project. @@ -363,4 +364,5 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [krzysiekj]: https://github.com/krzysiekj [ericbuehl]: https://github.com/ericbuehl [kristianoellegaard]: https://github.com/kristianoellegaard -[alexander-akhmetov]: htttps://github.com/alexander-akhmetov \ No newline at end of file +[alexander-akhmetov]: https://github.com/alexander-akhmetov +[niwibe]: https://github.com/niwibe From da9c17067c3150897da4cab149f12dee08768346 Mon Sep 17 00:00:00 2001 From: Brett Koonce Date: Thu, 29 Aug 2013 09:23:34 -0500 Subject: [PATCH 15/20] minor sp --- docs/api-guide/generic-views.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 931cae542..7185b6b68 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -73,7 +73,7 @@ The following attributes control the basic view behavior. **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_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: #### `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. From 11071499a777ecfee6edfb7e92ecf9a12d35eeb7 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Thu, 29 Aug 2013 18:10:47 +0200 Subject: [PATCH 16/20] Make ChoiceField.from_native() follow IntegerField behaviour on empty values --- rest_framework/fields.py | 5 +++++ rest_framework/tests/test_fields.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 3e0ca1a18..210c2537d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -514,6 +514,11 @@ class ChoiceField(WritableField): return True 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): type_name = 'EmailField' diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index ebccba7d1..34fbab9c9 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -688,6 +688,14 @@ class ChoiceFieldTests(TestCase): f = serializers.ChoiceField(required=False, choices=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): """ From 4b46de7dcebb31e9f7de11926ab5a4ecaa80c770 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 17:27:00 +0100 Subject: [PATCH 17/20] Added @diox for fix #1074. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 47807a0ed..b2d3d5d29 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -165,6 +165,7 @@ The following people have helped make REST framework great. * Kristian Øllegaard - [kristianoellegaard] * Alexander Akhmetov - [alexander-akhmetov] * Andrey Antukh - [niwibe] +* Mathieu Pillard - [diox] Many thanks to everyone who's contributed to the project. @@ -366,3 +367,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [kristianoellegaard]: https://github.com/kristianoellegaard [alexander-akhmetov]: https://github.com/alexander-akhmetov [niwibe]: https://github.com/niwibe +[diox]: https://github.com/diox From ac0fb01be3f33fab8d94117daf84a065f67bc343 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 17:27:08 +0100 Subject: [PATCH 18/20] Update release notes. --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 516efdc85..a901412f3 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -48,6 +48,7 @@ You can determine your currently installed version using `pip freeze`: * Bugfix: `required=True` argument fixed for boolean serializer fields. * 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: Empty values on ChoiceFields with `required=False` now consistently return `None`. ### 2.3.7 From 556b4bbba9a735cd372d5b12e9fdccd256643cb2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 20:04:00 +0100 Subject: [PATCH 19/20] Added note on botbot IRC archives --- docs/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index a0ae2984d..e0a2e911b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -200,7 +200,7 @@ To run the tests against all supported configurations, first install [the tox te ## 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. @@ -307,6 +307,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [tox]: http://testrun.org/tox/latest/ [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework +[botbot]: https://botbot.me/freenode/restframework/ [stack-overflow]: http://stackoverflow.com/ [django-rest-framework-tag]: http://stackoverflow.com/questions/tagged/django-rest-framework [django-tag]: http://stackoverflow.com/questions/tagged/django From 02b6836ee88498861521dfff743467b0456ad109 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 20:51:51 +0100 Subject: [PATCH 20/20] Fix breadcrumb view names --- rest_framework/utils/breadcrumbs.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py index 0384faba3..e6690d170 100644 --- a/rest_framework/utils/breadcrumbs.py +++ b/rest_framework/utils/breadcrumbs.py @@ -8,8 +8,11 @@ def get_breadcrumbs(url): tuple of (name, url). """ + from rest_framework.settings import api_settings from rest_framework.views import APIView + view_name_func = api_settings.VIEW_NAME_FUNCTION + def breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen): """ Add tuples of (name, url) to the breadcrumbs list, @@ -28,8 +31,8 @@ def get_breadcrumbs(url): # Don't list the same view twice in a row. # Probably an optional trailing slash. if not seen or seen[-1] != view: - instance = view.cls() - name = instance.get_view_name() + suffix = getattr(view, 'suffix', None) + name = view_name_func(cls, suffix) breadcrumbs_list.insert(0, (name, prefix + url)) seen.append(view)