From 90c9968a70e0a3d14cf4433cd356bcbdd30fce1b Mon Sep 17 00:00:00 2001 From: Michael Marvick Date: Sun, 25 Jan 2015 23:45:56 -0800 Subject: [PATCH 01/24] tutorial #1 incorrectly showed string of json instead of ReturnDict type from 'serializer.data', and also has a third item in the second usage --- docs/tutorial/1-serialization.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 80e869ea6..458161d07 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -151,7 +151,7 @@ We've now got a few snippet instances to play with. Let's take a look at serial serializer = SnippetSerializer(snippet) serializer.data - # {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'} + # ReturnDict([('pk', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]) At this point we've translated the model instance into Python native datatypes. To finalize the serialization process we render the data into `json`. @@ -182,7 +182,8 @@ We can also serialize querysets instead of model instances. To do so we simply serializer = SnippetSerializer(Snippet.objects.all(), many=True) serializer.data - # [{'pk': 1, 'title': u'', 'code': u'foo = "bar"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}, {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}] + # [OrderedDict([('pk', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('pk', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('pk', 3), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])] + ## Using ModelSerializers From 2a6937f381fe514e6cc9165c0aee200bf145788f Mon Sep 17 00:00:00 2001 From: Michael Marvick Date: Sun, 25 Jan 2015 23:46:27 -0800 Subject: [PATCH 02/24] tutorial #2 incorrectly showed /item.json instead of /item/.json for format suffixes --- docs/tutorial/2-requests-and-responses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index c04269695..9315a6644 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -96,7 +96,7 @@ Notice that we're no longer explicitly tying our requests or responses to a give ## Adding optional format suffixes to our URLs -To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints. Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [http://example.com/api/items/4.json][json-url]. +To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints. Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [http://example.com/api/items/4/.json][json-url]. Start by adding a `format` keyword argument to both of the views, like so. From 73bd0d539f24d45695615c25a072175c58a4cf98 Mon Sep 17 00:00:00 2001 From: Michael Marvick Date: Sun, 25 Jan 2015 23:47:01 -0800 Subject: [PATCH 03/24] tutorial #5 incorrectly referenced 'settings.py' instead of 'tutorial/settings.py' --- docs/tutorial/5-relationships-and-hyperlinked-apis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index 2841f03e9..740a4ce21 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -138,7 +138,7 @@ After adding all those names into our URLconf, our final `snippets/urls.py` file The list views for users and code snippets could end up returning quite a lot of instances, so really we'd like to make sure we paginate the results, and allow the API client to step through each of the individual pages. -We can change the default list style to use pagination, by modifying our `settings.py` file slightly. Add the following setting: +We can change the default list style to use pagination, by modifying our `tutorial/settings.py` file slightly. Add the following setting: REST_FRAMEWORK = { 'PAGINATE_BY': 10 From 7bb5fd270da98d8957efb4bf0e4bd4679ddbcf5f Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Wed, 4 Feb 2015 16:03:03 +0200 Subject: [PATCH 04/24] FIX: Don't default to list in method args Fixes @list_route and @detail_route so that they don't initialize their `methods` parameter as a list. In some cases the list gets cleared, and the result is that default parameter is now empty, and may get reused unexpectedly. --- rest_framework/decorators.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 325435b3f..a68227c14 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -109,10 +109,12 @@ def permission_classes(permission_classes): return decorator -def detail_route(methods=['get'], **kwargs): +def detail_route(methods=None, **kwargs): """ Used to mark a method on a ViewSet that should be routed for detail requests. """ + if methods is None: + methods = ['get'] def decorator(func): func.bind_to_methods = methods func.detail = True @@ -121,10 +123,12 @@ def detail_route(methods=['get'], **kwargs): return decorator -def list_route(methods=['get'], **kwargs): +def list_route(methods=None, **kwargs): """ Used to mark a method on a ViewSet that should be routed for list requests. """ + if methods is None: + methods = ['get'] def decorator(func): func.bind_to_methods = methods func.detail = False From 58e7bbc8ecad8016cc18f7dbd31b235cb515b785 Mon Sep 17 00:00:00 2001 From: Ofir Ovadia Date: Wed, 4 Feb 2015 16:08:41 +0200 Subject: [PATCH 05/24] Prefetching the user object when getting the token in TokenAuthentication. Since the user object is fetched 4 lines after getting Token from the database, this removes a DB query for each token-authenticated request. --- rest_framework/authentication.py | 2 +- tests/test_authentication.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 4832ad33b..f7601fb12 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -167,7 +167,7 @@ class TokenAuthentication(BaseAuthentication): def authenticate_credentials(self, key): try: - token = self.model.objects.get(key=key) + token = self.model.objects.select_related('user').get(key=key) except self.model.DoesNotExist: raise exceptions.AuthenticationFailed('Invalid token') diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 44837c4ef..caabcc214 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -202,6 +202,12 @@ class TokenAuthTests(TestCase): response = self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_post_json_makes_one_db_query(self): + """Ensure that authenticating a user using a token performs only one DB query""" + auth = "Token " + self.key + func_to_test = lambda: self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) + self.assertNumQueries(1, func_to_test) + def test_post_form_failing_token_auth(self): """Ensure POSTing form over token auth without correct credentials fails""" response = self.csrf_client.post('/token/', {'example': 'example'}) From d920683237bd2eb17d110a80fc09708a67340f01 Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Wed, 4 Feb 2015 16:13:30 +0200 Subject: [PATCH 06/24] Use inline if --- rest_framework/decorators.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index a68227c14..7604eae13 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -18,8 +18,7 @@ def api_view(http_method_names=None): Decorator that converts a function-based view into an APIView subclass. Takes a list of allowed methods for the view as an argument. """ - if http_method_names is None: - http_method_names = ['GET'] + http_method_names = ['GET'] if http_method_names is None else http_method_names def decorator(func): @@ -113,8 +112,8 @@ def detail_route(methods=None, **kwargs): """ Used to mark a method on a ViewSet that should be routed for detail requests. """ - if methods is None: - methods = ['get'] + methods = ['get'] if methods is None else methods + def decorator(func): func.bind_to_methods = methods func.detail = True @@ -127,8 +126,8 @@ def list_route(methods=None, **kwargs): """ Used to mark a method on a ViewSet that should be routed for list requests. """ - if methods is None: - methods = ['get'] + methods = ['get'] if methods is None else methods + def decorator(func): func.bind_to_methods = methods func.detail = False From e13d2af1374c8a2b2146e1126d9406bfb4bbd9ec Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Wed, 4 Feb 2015 16:26:23 +0200 Subject: [PATCH 07/24] Parens around if clause --- rest_framework/decorators.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 7604eae13..21de1acf4 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -18,7 +18,7 @@ def api_view(http_method_names=None): Decorator that converts a function-based view into an APIView subclass. Takes a list of allowed methods for the view as an argument. """ - http_method_names = ['GET'] if http_method_names is None else http_method_names + http_method_names = ['GET'] if (http_method_names is None) else http_method_names def decorator(func): @@ -112,7 +112,7 @@ def detail_route(methods=None, **kwargs): """ Used to mark a method on a ViewSet that should be routed for detail requests. """ - methods = ['get'] if methods is None else methods + methods = ['get'] if (methods is None) else methods def decorator(func): func.bind_to_methods = methods @@ -126,7 +126,7 @@ def list_route(methods=None, **kwargs): """ Used to mark a method on a ViewSet that should be routed for list requests. """ - methods = ['get'] if methods is None else methods + methods = ['get'] if (methods is None) else methods def decorator(func): func.bind_to_methods = methods From fffde8a63be7660e716672c500f0f2bd66c7d345 Mon Sep 17 00:00:00 2001 From: Kaptian Date: Thu, 5 Feb 2015 13:27:26 -0800 Subject: [PATCH 08/24] Update throttling.py Use pk pseudo attribute for identifying the user (in case the user model is not the default and has a different column name for the unique id) --- rest_framework/throttling.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 0f10136d6..261fc2463 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -191,7 +191,7 @@ class UserRateThrottle(SimpleRateThrottle): def get_cache_key(self, request, view): if request.user.is_authenticated(): - ident = request.user.id + ident = request.user.pk else: ident = self.get_ident(request) @@ -239,7 +239,7 @@ class ScopedRateThrottle(SimpleRateThrottle): with the '.throttle_scope` property of the view. """ if request.user.is_authenticated(): - ident = request.user.id + ident = request.user.pk else: ident = self.get_ident(request) From 5bf803b6ed260d9afde47400b7d5e8912a16ecf6 Mon Sep 17 00:00:00 2001 From: Michael Marvick Date: Thu, 5 Feb 2015 19:42:36 -0800 Subject: [PATCH 09/24] Revert some of the changes made in 1-serialization.md --- docs/tutorial/1-serialization.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 458161d07..80e869ea6 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -151,7 +151,7 @@ We've now got a few snippet instances to play with. Let's take a look at serial serializer = SnippetSerializer(snippet) serializer.data - # ReturnDict([('pk', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]) + # {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'} At this point we've translated the model instance into Python native datatypes. To finalize the serialization process we render the data into `json`. @@ -182,8 +182,7 @@ We can also serialize querysets instead of model instances. To do so we simply serializer = SnippetSerializer(Snippet.objects.all(), many=True) serializer.data - # [OrderedDict([('pk', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('pk', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('pk', 3), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])] - + # [{'pk': 1, 'title': u'', 'code': u'foo = "bar"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}, {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}] ## Using ModelSerializers From 75ff754517c30df043de906b0a6fb0e1777570b7 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 6 Feb 2015 10:12:57 +0100 Subject: [PATCH 10/24] Use twine to upload to pypi. --- setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index efe39d8d4..391987bc9 100755 --- a/setup.py +++ b/setup.py @@ -48,8 +48,11 @@ if sys.argv[-1] == 'publish': if os.system("pip freeze | grep wheel"): print("wheel not installed.\nUse `pip install wheel`.\nExiting.") sys.exit() - os.system("python setup.py sdist upload") - os.system("python setup.py bdist_wheel upload") + if os.system("pip freeze | grep twine") + print("twine not installed.\nUse `pip install twine`.\nExiting.") + sys.exit() + os.system("python setup.py sdist bdist_wheel") + os.system("twine upload dist/*") print("You probably want to also tag the version now:") print(" git tag -a %s -m 'version %s'" % (version, version)) print(" git push --tags") From 9dd97a0ee515089a1f818007f23460cf83159e71 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 6 Feb 2015 10:23:58 +0100 Subject: [PATCH 11/24] Fixed a typo. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 391987bc9..4cdcfa86e 100755 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ if sys.argv[-1] == 'publish': if os.system("pip freeze | grep wheel"): print("wheel not installed.\nUse `pip install wheel`.\nExiting.") sys.exit() - if os.system("pip freeze | grep twine") + if os.system("pip freeze | grep twine"): print("twine not installed.\nUse `pip install twine`.\nExiting.") sys.exit() os.system("python setup.py sdist bdist_wheel") From 238a3b507baa4543e3bae82a6fc9d88a0aadc5ea Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 6 Feb 2015 13:50:40 +0100 Subject: [PATCH 12/24] Add Twine to the requirements. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 00d973cdf..e5f555f5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ django-oauth2-provider>=0.2.4 # wheel for PyPI installs wheel==0.24.0 +twine==1.4.0 # MkDocs for documentation previews/deploys mkdocs==0.11.1 From 750d0c9f2b994af2ba92d2d470bbe079f9d9847c Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 6 Feb 2015 13:57:08 +0100 Subject: [PATCH 13/24] Add Twine to the requirements. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index e5f555f5e..32938ab23 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ django-oauth2-provider>=0.2.4 # wheel for PyPI installs wheel==0.24.0 +# twine for secured PyPI uploads twine==1.4.0 # MkDocs for documentation previews/deploys From d13c807616030b285589cec2fddf4e34a8e22b4a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Feb 2015 17:02:54 +0000 Subject: [PATCH 14/24] Fix misleading AttributeErrors --- rest_framework/request.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index cfbbdeccd..38fcf9c0a 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -18,6 +18,7 @@ from django.utils.six import BytesIO from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions from rest_framework.settings import api_settings +import sys import warnings @@ -485,8 +486,16 @@ class Request(object): else: self.auth = None - def __getattr__(self, attr): + def __getattribute__(self, attr): """ - Proxy other attributes to the underlying HttpRequest object. + If an attribute does not exist on this instance, then we also attempt + to proxy it to the underlying HttpRequest object. """ - return getattr(self._request, attr) + try: + return super(Request, self).__getattribute__(attr) + except AttributeError: + info = sys.exc_info() + try: + return getattr(self._request, attr) + except AttributeError: + raise info[0], info[1], info[2].tb_next From 54d82f59ed8a5d2ad4c679680dc52b8a94831d50 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Feb 2015 17:19:22 +0000 Subject: [PATCH 15/24] Py3 compat fix --- rest_framework/request.py | 8 ++++---- tests/test_request.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index 38fcf9c0a..c4de9424a 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -12,9 +12,9 @@ from __future__ import unicode_literals from django.conf import settings from django.http import QueryDict from django.http.multipartparser import parse_header +from django.utils import six from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MergeDict as DjangoMergeDict -from django.utils.six import BytesIO from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions from rest_framework.settings import api_settings @@ -363,7 +363,7 @@ class Request(object): elif hasattr(self._request, 'read'): self._stream = self._request else: - self._stream = BytesIO(self.raw_post_data) + self._stream = six.BytesIO(self.raw_post_data) def _perform_form_overloading(self): """ @@ -405,7 +405,7 @@ class Request(object): self._CONTENTTYPE_PARAM in self._data ): self._content_type = self._data[self._CONTENTTYPE_PARAM] - self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding'])) + self._stream = six.BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding'])) self._data, self._files, self._full_data = (Empty, Empty, Empty) def _parse(self): @@ -498,4 +498,4 @@ class Request(object): try: return getattr(self._request, attr) except AttributeError: - raise info[0], info[1], info[2].tb_next + six.reraise(info[0], info[1], info[2].tb_next) diff --git a/tests/test_request.py b/tests/test_request.py index 02a9b1e27..06ad8e937 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -249,6 +249,26 @@ class TestUserSetter(TestCase): login(self.request, self.user) self.assertEqual(self.wrapped_request.user, self.user) + def test_calling_user_fails_when_attribute_error_is_raised(self): + """ + This proves that when an AttributeError is raised inside of the request.user + property, that we can handle this and report the true, underlying error. + """ + class AuthRaisesAttributeError(object): + def authenticate(self, request): + import rest_framework + rest_framework.MISSPELLED_NAME_THAT_DOESNT_EXIST + + self.request = Request(factory.get('/'), authenticators=(AuthRaisesAttributeError(),)) + SessionMiddleware().process_request(self.request) + + login(self.request, self.user) + try: + self.request.user + except AttributeError as error: + self.assertEqual(str(error), "'module' object has no attribute 'MISSPELLED_NAME_THAT_DOESNT_EXIST'") + else: + assert False, 'AttributeError not raised' class TestAuthSetter(TestCase): From 0669f507b3a63114cf429f0662b1781f0e1fa5f8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Feb 2015 17:22:13 +0000 Subject: [PATCH 16/24] pep8 fix --- tests/test_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_request.py b/tests/test_request.py index 06ad8e937..c274ab69d 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -270,8 +270,8 @@ class TestUserSetter(TestCase): else: assert False, 'AttributeError not raised' -class TestAuthSetter(TestCase): +class TestAuthSetter(TestCase): def test_auth_can_be_set(self): request = Request(factory.get('/')) request.auth = 'DUMMY' From b2939c157d32e604e10099be891e382d8c54bbec Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Feb 2015 17:43:20 +0000 Subject: [PATCH 17/24] Fixes for latest version of pep8 --- env/pip-selfcheck.json | 1 + rest_framework/templatetags/rest_framework.py | 4 +++- tests/test_authentication.py | 5 ++++- tests/test_relations_hyperlink.py | 4 +++- tests/test_renderers.py | 9 +++++++-- tests/test_response.py | 9 +++++++-- tests/test_throttling.py | 8 ++++++-- 7 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 env/pip-selfcheck.json diff --git a/env/pip-selfcheck.json b/env/pip-selfcheck.json new file mode 100644 index 000000000..db1087af9 --- /dev/null +++ b/env/pip-selfcheck.json @@ -0,0 +1 @@ +{"last_check":"2015-02-09T17:34:33Z","pypi_version":"6.0.8"} \ No newline at end of file diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 69e03af40..d66ffb330 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -154,7 +154,9 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru If autoescape is True, the link text and URLs will get autoescaped. """ - trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x + def trim_url(x, limit=trim_url_limit): + return limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x + safe_input = isinstance(text, SafeData) words = word_split_re.split(force_text(text)) for i, word in enumerate(words): diff --git a/tests/test_authentication.py b/tests/test_authentication.py index caabcc214..19fe6043f 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -205,7 +205,10 @@ class TokenAuthTests(TestCase): def test_post_json_makes_one_db_query(self): """Ensure that authenticating a user using a token performs only one DB query""" auth = "Token " + self.key - func_to_test = lambda: self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) + + def func_to_test(): + return self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) + self.assertNumQueries(1, func_to_test) def test_post_form_failing_token_auth(self): diff --git a/tests/test_relations_hyperlink.py b/tests/test_relations_hyperlink.py index f1b882edf..2230c275c 100644 --- a/tests/test_relations_hyperlink.py +++ b/tests/test_relations_hyperlink.py @@ -12,7 +12,9 @@ factory = APIRequestFactory() request = factory.get('/') # Just to ensure we have a request in the serializer context -dummy_view = lambda request, pk: None +def dummy_view(request, pk): + pass + urlpatterns = patterns( '', diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 54eea8ceb..4f41144e5 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -28,8 +28,13 @@ import re DUMMYSTATUS = status.HTTP_200_OK DUMMYCONTENT = 'dummycontent' -RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii') -RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii') + +def RENDERER_A_SERIALIZER(x): + return ('Renderer A: %s' % x).encode('ascii') + + +def RENDERER_B_SERIALIZER(x): + return ('Renderer B: %s' % x).encode('ascii') expected_results = [ diff --git a/tests/test_response.py b/tests/test_response.py index f233ae332..4a9deaa29 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -38,8 +38,13 @@ class MockTextMediaRenderer(BaseRenderer): DUMMYSTATUS = status.HTTP_200_OK DUMMYCONTENT = 'dummycontent' -RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii') -RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii') + +def RENDERER_A_SERIALIZER(x): + return ('Renderer A: %s' % x).encode('ascii') + + +def RENDERER_B_SERIALIZER(x): + return ('Renderer B: %s' % x).encode('ascii') class RendererA(BaseRenderer): diff --git a/tests/test_throttling.py b/tests/test_throttling.py index cc36a004c..50a53b3eb 100644 --- a/tests/test_throttling.py +++ b/tests/test_throttling.py @@ -188,7 +188,9 @@ class ScopedRateThrottleTests(TestCase): class XYScopedRateThrottle(ScopedRateThrottle): TIMER_SECONDS = 0 THROTTLE_RATES = {'x': '3/min', 'y': '1/min'} - timer = lambda self: self.TIMER_SECONDS + + def timer(self): + return self.TIMER_SECONDS class XView(APIView): throttle_classes = (XYScopedRateThrottle,) @@ -290,7 +292,9 @@ class XffTestingBase(TestCase): class Throttle(ScopedRateThrottle): THROTTLE_RATES = {'test_limit': '1/day'} TIMER_SECONDS = 0 - timer = lambda self: self.TIMER_SECONDS + + def timer(self): + return self.TIMER_SECONDS class View(APIView): throttle_classes = (Throttle,) From 1a087c8c5bac6f157979ef9ff540c0eb23848fb4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Feb 2015 17:47:59 +0000 Subject: [PATCH 18/24] Fix .gitignore --- .gitignore | 18 +++++++----------- env/pip-selfcheck.json | 1 - 2 files changed, 7 insertions(+), 12 deletions(-) delete mode 100644 env/pip-selfcheck.json diff --git a/.gitignore b/.gitignore index 2bdf8f7eb..3d5f1043d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,18 +3,14 @@ *~ .* -site/ -htmlcov/ -coverage/ -build/ -dist/ -*.egg-info/ +/site/ +/htmlcov/ +/coverage/ +/build/ +/dist/ +/*.egg-info/ +/env/ MANIFEST -bin/ -include/ -lib/ -local/ - !.gitignore !.travis.yml diff --git a/env/pip-selfcheck.json b/env/pip-selfcheck.json deleted file mode 100644 index db1087af9..000000000 --- a/env/pip-selfcheck.json +++ /dev/null @@ -1 +0,0 @@ -{"last_check":"2015-02-09T17:34:33Z","pypi_version":"6.0.8"} \ No newline at end of file From 7b639c0cd0676172cc8502e833f5b708f39f9a83 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Feb 2015 17:57:08 +0000 Subject: [PATCH 19/24] Drop django master from the build matrix. Currently always going to be a know failure case. We can add it back when we start to consdier Django 1.9 support. --- .travis.yml | 12 ------------ tox.ini | 3 +-- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 28ebfc00f..4f9297853 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,18 +25,6 @@ env: - TOX_ENV=py33-django18alpha - TOX_ENV=py32-django18alpha - TOX_ENV=py27-django18alpha - - TOX_ENV=py34-djangomaster - - TOX_ENV=py33-djangomaster - - TOX_ENV=py32-djangomaster - - TOX_ENV=py27-djangomaster - -matrix: - fast_finish: true - allow_failures: - - env: TOX_ENV=py34-djangomaster - - env: TOX_ENV=py33-djangomaster - - env: TOX_ENV=py32-djangomaster - - env: TOX_ENV=py27-djangomaster install: - pip install tox diff --git a/tox.ini b/tox.ini index 8e0369643..eda92c19b 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py27-{flake8,docs}, {py26,py27}-django14, {py26,py27,py32,py33,py34}-django{15,16}, - {py27,py32,py33,py34}-django{17,18alpha,master} + {py27,py32,py33,py34}-django{17,18alpha} [testenv] commands = ./runtests.py --fast @@ -15,7 +15,6 @@ deps = django16: Django==1.6.3 # Should track minimum supported django17: Django==1.7.2 # Should track maximum supported django18alpha: https://www.djangoproject.com/download/1.8a1/tarball/ - djangomaster: https://github.com/django/django/zipball/master {py26,py27}-django{14,15,16,17}: django-guardian==1.2.3 {py26,py27}-django{14,15,16}: oauth2==1.5.211 {py26,py27}-django{14,15,16}: django-oauth-plus==2.2.1 From d87bb67d11918683425af1c1d56c0c57f50e81b3 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 10 Feb 2015 10:50:35 +0100 Subject: [PATCH 20/24] Failing test case for #1488 --- tests/test_filters.py | 50 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_filters.py b/tests/test_filters.py index 355f02cef..e7cb0c795 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -429,6 +429,56 @@ class SearchFilterTests(TestCase): reload_module(filters) +class AttributeModel(models.Model): + label = models.CharField(max_length=32) + + +class SearchFilterModelM2M(models.Model): + title = models.CharField(max_length=20) + text = models.CharField(max_length=100) + attributes = models.ManyToManyField(AttributeModel) + + +class SearchFilterM2MSerializer(serializers.ModelSerializer): + class Meta: + model = SearchFilterModelM2M + + +class SearchFilterM2MTests(TestCase): + def setUp(self): + # Sequence of title/text/attributes is: + # + # z abc [1, 2, 3] + # zz bcd [1, 2, 3] + # zzz cde [1, 2, 3] + # ... + for idx in range(3): + label = 'w' * (idx + 1) + AttributeModel(label=label) + + for idx in range(10): + title = 'z' * (idx + 1) + text = ( + chr(idx + ord('a')) + + chr(idx + ord('b')) + + chr(idx + ord('c')) + ) + SearchFilterModelM2M(title=title, text=text).save() + SearchFilterModelM2M.objects.get(title='zz').attributes.add(1, 2, 3) + + def test_m2m_search(self): + class SearchListView(generics.ListAPIView): + queryset = SearchFilterModelM2M.objects.all() + serializer_class = SearchFilterM2MSerializer + filter_backends = (filters.SearchFilter,) + search_fields = ('=title', 'text', 'attributes__label') + + view = SearchListView.as_view() + request = factory.get('/', {'search': 'zz'}) + response = view(request) + self.assertEqual(len(response.data), 1) + + class OrderingFilterModel(models.Model): title = models.CharField(max_length=20) text = models.CharField(max_length=100) From 3522b69394d932c8bf8028a456b6d9b64c38b54e Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 10 Feb 2015 10:51:38 +0100 Subject: [PATCH 21/24] Add `distinct` call in `filter_queryset` --- rest_framework/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index d188a2d1e..d3f55a447 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -104,7 +104,7 @@ class SearchFilter(BaseFilterBackend): for search_term in self.get_search_terms(request): or_queries = [models.Q(**{orm_lookup: search_term}) for orm_lookup in orm_lookups] - queryset = queryset.filter(reduce(operator.or_, or_queries)) + queryset = queryset.filter(reduce(operator.or_, or_queries)).distinct() return queryset From f6033cee87e367ec3a6ffcdd6897656b3e3c0493 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 10 Feb 2015 22:36:41 +0100 Subject: [PATCH 22/24] Add release notes for 3.0.5. --- docs/topics/release-notes.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index e0894d2d9..e74dc803f 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -41,6 +41,19 @@ You can determine your currently installed version using `pip freeze`: ## 3.0.x series +### 3.0.5 + +**Date**: [10th February 2015][3.0.5-milestone]. + +* Fix a bug where `_closable_objects` breaks pickling. ([#1850][gh1850], [#2492][gh2492]) +* Allow non-standard `User` models with `Throttling`. ([#2524][gh2524]) +* Support custom `User.db_table` in TokenAuthentication migration. ([#2479][gh2479]) +* Fix misleading `AttributeError` tracebacks on `Request` objects. ([#2530][gh2530], [#2108][gh2108]) +* `ManyRelatedField.get_value` clearing field on partial update. ([#2475][gh2475]) +* Removed '.model' shortcut from code. ([#2486][gh2486]) +* Fix `detail_route` and `list_route` mutable argument. ([#2518][gh2518]) +* Prefetching the user object when getting the token in `TokenAuthentication`. ([#2519][gh2519]) + ### 3.0.4 **Date**: [28th January 2015][3.0.4-milestone]. @@ -721,6 +734,7 @@ For older release notes, [please see the GitHub repo](old-release-notes). [3.0.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.2+Release%22 [3.0.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.3+Release%22 [3.0.4-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.4+Release%22 +[3.0.5-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.5+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -808,3 +822,14 @@ For older release notes, [please see the GitHub repo](old-release-notes). [gh2399]: https://github.com/tomchristie/django-rest-framework/issues/2399 [gh2388]: https://github.com/tomchristie/django-rest-framework/issues/2388 [gh2360]: https://github.com/tomchristie/django-rest-framework/issues/2360 + +[gh1850]: https://github.com/tomchristie/django-rest-framework/issues/1850 +[gh2108]: https://github.com/tomchristie/django-rest-framework/issues/2108 +[gh2475]: https://github.com/tomchristie/django-rest-framework/issues/2475 +[gh2479]: https://github.com/tomchristie/django-rest-framework/issues/2479 +[gh2486]: https://github.com/tomchristie/django-rest-framework/issues/2486 +[gh2492]: https://github.com/tomchristie/django-rest-framework/issues/2492 +[gh2518]: https://github.com/tomchristie/django-rest-framework/issues/2518 +[gh2519]: https://github.com/tomchristie/django-rest-framework/issues/2519 +[gh2524]: https://github.com/tomchristie/django-rest-framework/issues/2524 +[gh2530]: https://github.com/tomchristie/django-rest-framework/issues/2530 From 59b3fe8f395b2d6ee4091df81f9dbbc7e47cf84e Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 10 Feb 2015 22:48:04 +0100 Subject: [PATCH 23/24] Bumped the version to 3.0.5 --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 57e5421b8..9b58f09f4 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.0.4' +__version__ = '3.0.5' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2015 Tom Christie' From 9d80335ac86076f75c81de02abc0cda8f98916d7 Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Thu, 12 Feb 2015 01:10:03 +0100 Subject: [PATCH 24/24] Remove '.model' shortcut from viewset docs. Refs #2486. Closes #2549. --- docs/api-guide/viewsets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index bbf92c6ce..4fd7aa84c 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -178,7 +178,7 @@ The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`, #### Example -Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes, or the `model` attribute shortcut. For example: +Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes. For example: class AccountViewSet(viewsets.ModelViewSet): """