From 921e4ed2ee11edffd19d2ca40f10d47d2c148ea1 Mon Sep 17 00:00:00 2001 From: Paul Oswald Date: Mon, 28 Jul 2014 16:59:55 +0900 Subject: [PATCH 01/62] Evaluate content before passing to regex.sub Issue #1708 --- rest_framework/tests/test_description.py | 22 ++++++++++++++++++++++ rest_framework/utils/formatting.py | 4 +--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/rest_framework/tests/test_description.py b/rest_framework/tests/test_description.py index 4c03c1ded..52fa55fb8 100644 --- a/rest_framework/tests/test_description.py +++ b/rest_framework/tests/test_description.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.test import TestCase +from django.utils.encoding import python_2_unicode_compatible from rest_framework.compat import apply_markdown, smart_text from rest_framework.views import APIView from rest_framework.tests.description import ViewWithNonASCIICharactersInDocstring @@ -98,6 +99,27 @@ class TestViewNamesAndDescriptions(TestCase): pass self.assertEqual(MockView().get_view_description(), '') + def test_view_description_can_be_promise(self): + """ + Ensure a view may have a docstring that is actually a lazily evaluated + class that can be converted to a string. + + See: https://github.com/tomchristie/django-rest-framework/issues/1708 + """ + # use a mock object instead of gettext_lazy to ensure that we can't end + # up with a test case string in our l10n catalog + @python_2_unicode_compatible + class MockLazyStr(object): + def __init__(self, string): + self.s = string + def __str__(self): + return self.s + + class MockView(APIView): + __doc__ = MockLazyStr("a gettext string") + + self.assertEqual(MockView().get_view_description(), 'a gettext string') + def test_markdown(self): """ Ensure markdown to HTML works as expected. diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 4b59ba840..12b79b6cd 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -6,8 +6,6 @@ from __future__ import unicode_literals from django.utils.html import escape from django.utils.safestring import mark_safe from rest_framework.compat import apply_markdown -from rest_framework.settings import api_settings -from textwrap import dedent import re @@ -36,7 +34,7 @@ def dedent(content): # unindent the content if needed if whitespace_counts: whitespace_pattern = '^' + (' ' * min(whitespace_counts)) - content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) + content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', unicode(content)) return content.strip() From 66fa40c300b4d3e768b4a7993f020056c44fdda3 Mon Sep 17 00:00:00 2001 From: Paul Oswald Date: Tue, 29 Jul 2014 22:13:11 +0900 Subject: [PATCH 02/62] evaluate content at function start --- rest_framework/utils/formatting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 12b79b6cd..2b3cbc957 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -28,13 +28,14 @@ def dedent(content): as it fails to dedent multiline docstrings that include unindented text on the initial line. """ + content = unicode(content) whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in content.splitlines()[1:] if line.lstrip()] # unindent the content if needed if whitespace_counts: whitespace_pattern = '^' + (' ' * min(whitespace_counts)) - content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', unicode(content)) + content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) return content.strip() From 192201d5840f13c8b96f44fdce4645edeb653f0f Mon Sep 17 00:00:00 2001 From: Paul Oswald Date: Thu, 7 Aug 2014 15:48:29 +0900 Subject: [PATCH 03/62] remove dep on python_2_unicode_compatible python_2_unicode_compatible is not available in all Django versions --- rest_framework/tests/test_description.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/tests/test_description.py b/rest_framework/tests/test_description.py index 52fa55fb8..8aa162613 100644 --- a/rest_framework/tests/test_description.py +++ b/rest_framework/tests/test_description.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals from django.test import TestCase -from django.utils.encoding import python_2_unicode_compatible from rest_framework.compat import apply_markdown, smart_text from rest_framework.views import APIView from rest_framework.tests.description import ViewWithNonASCIICharactersInDocstring @@ -108,17 +107,18 @@ class TestViewNamesAndDescriptions(TestCase): """ # use a mock object instead of gettext_lazy to ensure that we can't end # up with a test case string in our l10n catalog - @python_2_unicode_compatible class MockLazyStr(object): def __init__(self, string): self.s = string def __str__(self): return self.s + def __unicode__(self): + return self.s class MockView(APIView): - __doc__ = MockLazyStr("a gettext string") + __doc__ = MockLazyStr(u"a gettext string") - self.assertEqual(MockView().get_view_description(), 'a gettext string') + self.assertEqual(MockView().get_view_description(), u'a gettext string') def test_markdown(self): """ From 3e93c96ece8af010185e1fe1188dd2df569d4528 Mon Sep 17 00:00:00 2001 From: Paul Oswald Date: Tue, 19 Aug 2014 10:09:48 +0900 Subject: [PATCH 04/62] replace unicode call with force_text --- rest_framework/utils/formatting.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 2b3cbc957..40bced5f1 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -5,6 +5,8 @@ from __future__ import unicode_literals from django.utils.html import escape from django.utils.safestring import mark_safe +from django.utils.encoding import force_text + from rest_framework.compat import apply_markdown import re @@ -28,7 +30,7 @@ def dedent(content): as it fails to dedent multiline docstrings that include unindented text on the initial line. """ - content = unicode(content) + content = force_text(content) whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in content.splitlines()[1:] if line.lstrip()] From baceb528cb80584285592aa0160b33101cb0ca37 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Fri, 29 Aug 2014 11:11:18 +0100 Subject: [PATCH 05/62] Fix typos in 2.4 release notes --- docs/topics/2.4-announcement.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/2.4-announcement.md b/docs/topics/2.4-announcement.md index 5f90319ab..ab11360f2 100644 --- a/docs/topics/2.4-announcement.md +++ b/docs/topics/2.4-announcement.md @@ -17,7 +17,7 @@ The optional authtoken application now includes support for *both* Django 1.7 sc ## Deprecation of `.model` view attribute -The `.model` attribute on view classes is an optional shortcut for either or both of `.serializer_class` and `.queryset`. It's usage results in more implicit, less obvious behavior. +The `.model` attribute on view classes is an optional shortcut for either or both of `.serializer_class` and `.queryset`. Its usage results in more implicit, less obvious behavior. The documentation has previously stated that usage of the more explicit style is prefered, and we're now taking that one step further and deprecating the usage of the `.model` shortcut. @@ -128,7 +128,7 @@ There are also a number of other features and bugfixes as [listed in the release Smarter [client IP identification for throttling][client-ip-identification], with the addition of the `NUM_PROXIES` setting. -Added the standardized `Retry-After` header to throttled responses, as per [RFC 6585](http://tools.ietf.org/html/rfc6585). This should now be used in preference to the custom `X-Trottle-Wait-Seconds` header which will be fully deprecated in 3.0. +Added the standardized `Retry-After` header to throttled responses, as per [RFC 6585](http://tools.ietf.org/html/rfc6585). This should now be used in preference to the custom `X-Throttle-Wait-Seconds` header which will be fully deprecated in 3.0. ## Deprecations From 5490fc2700e4ca459583a3a00f253c1d85a7a189 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 29 Aug 2014 13:29:51 +0100 Subject: [PATCH 06/62] Fix links in 2.4 release --- docs/topics/2.4-announcement.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/topics/2.4-announcement.md b/docs/topics/2.4-announcement.md index ab11360f2..09294b910 100644 --- a/docs/topics/2.4-announcement.md +++ b/docs/topics/2.4-announcement.md @@ -163,10 +163,10 @@ The next planned release will be 3.0, featuring an improved and simplified seria Once again, many thanks to all the generous [backers and sponsors][kickstarter-sponsors] who've helped make this possible! [lts-releases]: https://docs.djangoproject.com/en/dev/internals/release-process/#long-term-support-lts-releases -[2-4-release-notes]: ./topics/release-notes/#240 +[2-4-release-notes]: release-notes#240 [view-name-and-description-settings]: ../api-guide/settings/#view-names-and-descriptions [client-ip-identification]: ../api-guide/throttling/#how-clients-are-identified -[2-3-announcement]: ./topics/2.3-announcement +[2-3-announcement]: 2.3-announcement [github-labels]: https://github.com/tomchristie/django-rest-framework/issues [github-milestones]: https://github.com/tomchristie/django-rest-framework/milestones -[kickstarter-sponsors]: ./topics/kickstarter-announcement/#sponsors +[kickstarter-sponsors]: kickstarter-announcement#sponsors From efaa37376ca0bb6f2442b633665ff8d3264e89d6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 29 Aug 2014 14:01:54 +0100 Subject: [PATCH 07/62] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d33177399..63513f758 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Startup up a new project like so... pip install django pip install djangorestframework - django-admin startproject example . + django-admin.py startproject example . ./manage.py syncdb Now edit the `example/urls.py` module in your project: From b554c67d14fb0464106a247e5da96af80b819be9 Mon Sep 17 00:00:00 2001 From: Daniel Roseman Date: Sat, 30 Aug 2014 13:28:12 +0100 Subject: [PATCH 08/62] Restore body block to base template. --- rest_framework/templates/rest_framework/base.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index cee9724d5..e54e38148 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -25,6 +25,7 @@ {% endblock %} + {% block body %}
@@ -261,4 +262,5 @@ {% endblock %} + {% endblock %} From 4dd4538069bb0cdf27db09799bc99933dd2972e1 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sun, 31 Aug 2014 03:39:34 +0200 Subject: [PATCH 09/62] Exclude the pyc, pyo files and __pycache__ directories from packaging (thanks to Kevin Brown). --- MANIFEST.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 15c4d0b08..d407865fb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,4 @@ recursive-include rest_framework/static *.js *.css *.png recursive-include rest_framework/templates *.html +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] From 1c9c5d5c32656231acf5f14b5231f9274a2eb254 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 1 Sep 2014 10:07:05 +0200 Subject: [PATCH 10/62] Regression for #1810: Test login view renders --- rest_framework/urls.py | 2 +- tests/conftest.py | 1 + tests/test_authentication.py | 13 ++++++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/rest_framework/urls.py b/rest_framework/urls.py index 8fa3073e8..cfcee534b 100644 --- a/rest_framework/urls.py +++ b/rest_framework/urls.py @@ -6,7 +6,7 @@ your API requires authentication: urlpatterns = patterns('', ... - url(r'^auth', include('rest_framework.urls', namespace='rest_framework')) + url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')) ) The urls must be namespaced as 'rest_framework', and you should make sure diff --git a/tests/conftest.py b/tests/conftest.py index f3723aeae..4b33e19c1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ def pytest_configure(): DEBUG_PROPAGATE_EXCEPTIONS=True, DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:'}}, + SITE_ID=1, SECRET_KEY='not very secret in tests', USE_I18N=True, USE_L10N=True, diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 2b9d73e4c..8294189e0 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -57,7 +57,8 @@ urlpatterns = patterns( authentication_classes=[OAuthAuthentication], permission_classes=[permissions.TokenHasReadWriteScope] ) - ) + ), + url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')) ) @@ -134,6 +135,16 @@ class SessionAuthTests(TestCase): def tearDown(self): self.csrf_client.logout() + def test_login_view_renders_on_get(self): + """ + Ensure the login template renders for a basic GET. + + cf. [#1810](https://github.com/tomchristie/django-rest-framework/pull/1810) + """ + response = self.csrf_client.get('/auth/login/') + self.assertContains(response, '') + + def test_post_form_session_auth_failing_csrf(self): """ Ensure POSTing form over session authentication without CSRF token fails. From 55e779c856347094e3240bc7bf83927acf0bd442 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Sep 2014 09:07:55 +0100 Subject: [PATCH 11/62] Version 2.4.1 --- docs/topics/release-notes.md | 6 ++++++ rest_framework/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 29a0afcd3..f0e9f2101 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,12 @@ You can determine your currently installed version using `pip freeze`: ## 2.4.x series +### 2.4.1 + +**Date**: 1st September 2014 + +* Bugfix: Fix broken login template for browsable API. + ### 2.4.0 **Date**: 29th August 2014 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index f95bdc22e..7c187639c 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '2.4.0' +__version__ = '2.4.1' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2014 Tom Christie' From 0e51dab8f4cdfeb05b7c70a0ca74ffa90d01f512 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 1 Sep 2014 10:09:46 +0200 Subject: [PATCH 12/62] Comform to flake8 --- tests/test_authentication.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 8294189e0..32041f9c1 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -144,7 +144,6 @@ class SessionAuthTests(TestCase): response = self.csrf_client.get('/auth/login/') self.assertContains(response, '') - def test_post_form_session_auth_failing_csrf(self): """ Ensure POSTing form over session authentication without CSRF token fails. From 278df84a7ceca29b0f3ffa6f3c87b4babe70fad0 Mon Sep 17 00:00:00 2001 From: Timo Tuominen Date: Mon, 1 Sep 2014 12:14:26 +0300 Subject: [PATCH 13/62] Correct testing documentation details. --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff6018b82..a6dd05a0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,10 +62,10 @@ To run the tests, clone the repository, and then: virtualenv env env/bin/activate pip install -r requirements.txt - pip install -r optionals.txt + pip install -r requirements-test.txt # Run the tests - py.test + ./runtests.py You can also use the excellent [`tox`][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: From 82d4b2083292659358d5df4d03d2115576e8ae4e Mon Sep 17 00:00:00 2001 From: Timo Tuominen Date: Mon, 1 Sep 2014 12:17:36 +0300 Subject: [PATCH 14/62] Add subclass matching to serializer field mapping. --- rest_framework/serializers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index be8ad3f24..6d25161e2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -907,6 +907,9 @@ class ModelSerializer(Serializer): try: return self.field_mapping[model_field.__class__](**kwargs) except KeyError: + for model_field_class, serializer_field_class in self.field_mapping.items(): + if isinstance(model_field, model_field_class): + return serializer_field_class(**kwargs) return ModelField(model_field=model_field, **kwargs) def get_validation_exclusions(self, instance=None): From ae84b8b0e8a99261ea2436f77ab5238f21603c0c Mon Sep 17 00:00:00 2001 From: Timo Tuominen Date: Mon, 1 Sep 2014 15:03:39 +0300 Subject: [PATCH 15/62] Traverse the method resolution order when mapping serializer fields. --- rest_framework/serializers.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6d25161e2..f37fbf980 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -904,13 +904,11 @@ class ModelSerializer(Serializer): for attribute in attributes: kwargs.update({attribute: getattr(model_field, attribute)}) - try: - return self.field_mapping[model_field.__class__](**kwargs) - except KeyError: - for model_field_class, serializer_field_class in self.field_mapping.items(): - if isinstance(model_field, model_field_class): - return serializer_field_class(**kwargs) - return ModelField(model_field=model_field, **kwargs) + for model_field_baseclass in inspect.getmro(model_field.__class__): + serializer_field_class = self.field_mapping.get(model_field_baseclass) + if serializer_field_class: + return serializer_field_class(**kwargs) + return ModelField(model_field=model_field, **kwargs) def get_validation_exclusions(self, instance=None): """ From 582f6fdd4b0fb12a7c0d1fefe265499a284c9b79 Mon Sep 17 00:00:00 2001 From: Timo Tuominen Date: Mon, 1 Sep 2014 15:54:33 +0300 Subject: [PATCH 16/62] Add utility function to match classes in dictionary. --- rest_framework/serializers.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f37fbf980..5c33300c4 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -625,6 +625,21 @@ class ModelSerializerOptions(SerializerOptions): self.write_only_fields = getattr(meta, 'write_only_fields', ()) +def _get_class_mapping(mapping, obj): + """ + Takes a dictionary with classes as keys, and an object. + Traverses the object's inheritance hierarchy in method + resolution order, and returns the first matching value + from the dictionary or None. + + """ + for baseclass in inspect.getmro(obj.__class__): + val = mapping.get(baseclass) + if val: + return val + return None + + class ModelSerializer(Serializer): """ A serializer that deals with model instances and querysets. @@ -899,15 +914,16 @@ class ModelSerializer(Serializer): models.URLField: ['max_length'], } - if model_field.__class__ in attribute_dict: - attributes = attribute_dict[model_field.__class__] + attributes = _get_class_mapping(attribute_dict, model_field) + if attributes: for attribute in attributes: kwargs.update({attribute: getattr(model_field, attribute)}) - for model_field_baseclass in inspect.getmro(model_field.__class__): - serializer_field_class = self.field_mapping.get(model_field_baseclass) - if serializer_field_class: - return serializer_field_class(**kwargs) + serializer_field_class = _get_class_mapping( + self.field_mapping, model_field) + + if serializer_field_class: + return serializer_field_class(**kwargs) return ModelField(model_field=model_field, **kwargs) def get_validation_exclusions(self, instance=None): From e437520217e20d500d641b95482d49484b1f24a7 Mon Sep 17 00:00:00 2001 From: Timo Tuominen Date: Mon, 1 Sep 2014 17:02:48 +0300 Subject: [PATCH 17/62] Generator implementation of class mapping. --- rest_framework/serializers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5c33300c4..b3db35823 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -633,11 +633,10 @@ def _get_class_mapping(mapping, obj): from the dictionary or None. """ - for baseclass in inspect.getmro(obj.__class__): - val = mapping.get(baseclass) - if val: - return val - return None + return next( + (mapping[cls] for cls in inspect.getmro(obj.__class__) if cls in mapping), + None + ) class ModelSerializer(Serializer): From fa0ef1773773c58b5708abad0e90a44fc9a308f8 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 2 Sep 2014 14:53:37 +0200 Subject: [PATCH 18/62] Remove Login Dropdown when Auth Views are not registered. Fixes #1738 --- .../templates/rest_framework/base.html | 26 +++----- rest_framework/templatetags/rest_framework.py | 17 +++-- tests/browsable_api/__init__.py | 0 tests/browsable_api/auth_urls.py | 10 +++ tests/browsable_api/no_auth_urls.py | 9 +++ tests/browsable_api/test_browsable_api.py | 65 +++++++++++++++++++ tests/browsable_api/views.py | 15 +++++ 7 files changed, 121 insertions(+), 21 deletions(-) create mode 100644 tests/browsable_api/__init__.py create mode 100644 tests/browsable_api/auth_urls.py create mode 100644 tests/browsable_api/no_auth_urls.py create mode 100644 tests/browsable_api/test_browsable_api.py create mode 100644 tests/browsable_api/views.py diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index e54e38148..5a12277bc 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -5,14 +5,14 @@ {% block head %} - + {% block meta %} {% endblock %} - + {% block title %}Django REST framework{% endblock %} - + {% block style %} {% block bootstrap_theme %} @@ -21,7 +21,7 @@ {% endblock %} - + {% endblock %} @@ -44,15 +44,7 @@
{% endif %} - + {% if put_form or raw_data_put_form or raw_data_patch_form %}
{% if put_form %} @@ -246,7 +238,7 @@ {% endif %}
- +