From aeed2cf3b23161f228c8b221e56ea4d8a7cf71aa Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Mon, 9 Sep 2013 07:59:35 -0400 Subject: [PATCH 01/58] Added a test to show that the user.is_staff check in admin base.html is necessary. refs #21067 --- tests/admin_views/tests.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 18d06f075c..ac289c550a 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -1296,6 +1296,19 @@ class AdminViewPermissionsTest(TestCase): response = self.client.get('/test_admin/admin/secure-view/') self.assertContains(response, 'id="login-form"') + def testDisabledStaffPermissionsWhenLoggedIn(self): + self.client.login(username='super', password='secret') + superuser = User.objects.get(username='super') + superuser.is_staff = False + superuser.save() + + response = self.client.get('/test_admin/admin/') + self.assertContains(response, 'id="login-form"') + self.assertNotContains(response, 'Log out') + + response = self.client.get('/test_admin/admin/secure-view/') + self.assertContains(response, 'id="login-form"') + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewsNoUrlTest(TestCase): From 6dca603abb0eb164ba87657caf5cc65bca449719 Mon Sep 17 00:00:00 2001 From: Daniel Boeve <djboeve@gmail.com> Date: Fri, 6 Sep 2013 18:47:08 +0000 Subject: [PATCH 02/58] Fixed #20889 -- Prevented email.Header from inserting newlines Passed large maxlinelen to email.Header to prevent newlines from being inserted into value returned by _convert_to_charset Thanks mjl at laubach.at for the report. --- django/http/response.py | 3 ++- tests/httpwrappers/tests.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/django/http/response.py b/django/http/response.py index 822589fe81..5cdedeca49 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import datetime import time +import sys from email.header import Header try: from urllib.parse import urlparse @@ -160,7 +161,7 @@ class HttpResponseBase(six.Iterator): except UnicodeError as e: if mime_encode: # Wrapping in str() is a workaround for #12422 under Python 2. - value = str(Header(value, 'utf-8').encode()) + value = str(Header(value, 'utf-8', maxlinelen=sys.maxsize).encode()) else: e.reason += ', HTTP response headers must be in %s format' % charset raise diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py index 356818d2ef..0d9611ef0e 100644 --- a/tests/httpwrappers/tests.py +++ b/tests/httpwrappers/tests.py @@ -290,6 +290,13 @@ class HttpResponseTests(unittest.TestCase): self.assertRaises(UnicodeError, r.__setitem__, 'føø', 'bar') self.assertRaises(UnicodeError, r.__setitem__, 'føø'.encode('utf-8'), 'bar') + def test_long_line(self): + # Bug #20889: long lines trigger newlines to be added to headers + # (which is not allowed due to bug #10188) + h = HttpResponse() + f = 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz a\xcc\x88'.encode('latin-1') + f = f.decode('utf-8') + h['Content-Disposition'] = u'attachment; filename="%s"' % f def test_newlines_in_headers(self): # Bug #10188: Do not allow newlines in headers (CR or LF) From 7c6f2ddcd9ab0d4a3a134da3f62e3d115045b4b9 Mon Sep 17 00:00:00 2001 From: Curtis Maloney <curtis@tinbrain.net> Date: Wed, 4 Sep 2013 20:53:55 +1000 Subject: [PATCH 03/58] Simplify FilterExpression.args_check --- django/template/base.py | 31 +++++----------------- tests/template_tests/test_parser.py | 41 ++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/django/template/base.py b/django/template/base.py index 9e3ea0f3c6..1265040fc0 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -622,34 +622,17 @@ class FilterExpression(object): def args_check(name, func, provided): provided = list(provided) - plen = len(provided) + # First argument, filter input, is implied. + plen = len(provided) + 1 # Check to see if a decorator is providing the real function. func = getattr(func, '_decorated_function', func) args, varargs, varkw, defaults = getargspec(func) - # First argument is filter input. - args.pop(0) - if defaults: - nondefs = args[:-len(defaults)] - else: - nondefs = args - # Args without defaults must be provided. - try: - for arg in nondefs: - provided.pop(0) - except IndexError: - # Not enough + alen = len(args) + dlen = len(defaults or []) + # Not enough OR Too many + if plen < (alen - dlen) or plen > alen: raise TemplateSyntaxError("%s requires %d arguments, %d provided" % - (name, len(nondefs), plen)) - - # Defaults can be overridden. - defaults = list(defaults) if defaults else [] - try: - for parg in provided: - defaults.pop(0) - except IndexError: - # Too many. - raise TemplateSyntaxError("%s requires %d arguments, %d provided" % - (name, len(nondefs), plen)) + (name, alen - dlen, plen)) return True args_check = staticmethod(args_check) diff --git a/tests/template_tests/test_parser.py b/tests/template_tests/test_parser.py index e626bfd34f..159a240530 100644 --- a/tests/template_tests/test_parser.py +++ b/tests/template_tests/test_parser.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals from unittest import TestCase from django.template import (TokenParser, FilterExpression, Parser, Variable, - Template, TemplateSyntaxError) + Template, TemplateSyntaxError, Library) from django.test.utils import override_settings from django.utils import six @@ -94,3 +94,42 @@ class ParserTests(TestCase): with six.assertRaisesRegex(self, TemplateSyntaxError, msg) as cm: Template("{% if 1 %}{{ foo@bar }}{% endif %}") self.assertEqual(cm.exception.django_template_source[1], (10, 23)) + + def test_filter_args_count(self): + p = Parser("") + l = Library() + @l.filter + def no_arguments(value): + pass + @l.filter + def one_argument(value, arg): + pass + @l.filter + def one_opt_argument(value, arg=False): + pass + @l.filter + def two_arguments(value, arg, arg2): + pass + @l.filter + def two_one_opt_arg(value, arg, arg2=False): + pass + p.add_library(l) + for expr in ( + '1|no_arguments:"1"', + '1|two_arguments', + '1|two_arguments:"1"', + '1|two_one_opt_arg', + ): + with self.assertRaises(TemplateSyntaxError): + FilterExpression(expr, p) + for expr in ( + # Correct number of arguments + '1|no_arguments', + '1|one_argument:"1"', + # One optional + '1|one_opt_argument', + '1|one_opt_argument:"1"', + # Not supplying all + '1|two_one_opt_arg:"1"', + ): + FilterExpression(expr, p) From 5e1c7d4cf6019772d861ea8408b60377b5352e71 Mon Sep 17 00:00:00 2001 From: Curtis Maloney <curtis@tinbrain.net> Date: Sun, 8 Sep 2013 13:11:57 +1000 Subject: [PATCH 04/58] Add myself to authors file --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 461182d77b..d43715f119 100644 --- a/AUTHORS +++ b/AUTHORS @@ -397,6 +397,7 @@ answer newbie questions, and generally made Django that much better: Yann Malet Frantisek Malina <vizualbod@vizualbod.com> Mike Malone <mjmalone@gmail.com> + Curtis Maloney (FunkyBob) <curtis@tinbrain.net> Martin Maney <http://www.chipy.org/Martin_Maney> Michael Manfre <mmanfre@gmail.com> Javier Mansilla <javimansilla@gmail.com> From 1185370c2c7e66428c45c69e960d10198e17e367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B0=D0=B4=D0=BE=D0=B2=D1=81=D0=BA=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=BE=D0=BB=D0=B0=D0=B9?= <sns1081@gmail.com> Date: Sat, 6 Jul 2013 09:38:33 +0700 Subject: [PATCH 05/58] Fixed #20707 -- Added explicit quota assignment to Oracle test user To enable testing on Oracle 12c --- django/db/backends/oracle/creation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py index b1a8782aa9..fc9a0d034f 100644 --- a/django/db/backends/oracle/creation.py +++ b/django/db/backends/oracle/creation.py @@ -181,6 +181,7 @@ class DatabaseCreation(BaseDatabaseCreation): IDENTIFIED BY %(password)s DEFAULT TABLESPACE %(tblspace)s TEMPORARY TABLESPACE %(tblspace_temp)s + QUOTA UNLIMITED ON %(tblspace)s """, """GRANT CONNECT, RESOURCE TO %(user)s""", ] From cbf08c6b0ca39406cada4bd1ffc6d334a79822e8 Mon Sep 17 00:00:00 2001 From: e0ne <e0ne@e0ne.info> Date: Sun, 8 Sep 2013 22:57:36 +0300 Subject: [PATCH 06/58] Fixed #16895 -- Warned about cost of QuerySet ordering Thanks outofculture at gmail.com for the suggestion. --- docs/ref/models/options.txt | 6 ++++++ docs/ref/models/querysets.txt | 6 ++++++ docs/topics/db/optimization.txt | 11 +++++++++++ 3 files changed, 23 insertions(+) diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index ee7d9d7116..1caeadddf9 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -221,6 +221,12 @@ Django quotes column and table names behind the scenes. ordering = ['-pub_date', 'author'] +.. warning:: + + Ordering is not a free operation. Each field you add to the ordering + incurs a cost to your database. Each foreign key you add will + impliclty include all of its default orderings as well. + ``permissions`` --------------- diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index c99d27769f..000415f028 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -334,6 +334,12 @@ You can tell if a query is ordered or not by checking the :attr:`.QuerySet.ordered` attribute, which will be ``True`` if the ``QuerySet`` has been ordered in any way. +.. warning:: + + Ordering is not a free operation. Each field you add to the ordering + incurs a cost to your database. Each foreign key you add will + impliclty include all of its default orderings as well. + reverse ~~~~~~~ diff --git a/docs/topics/db/optimization.txt b/docs/topics/db/optimization.txt index c99c84d9fa..ea919d76f8 100644 --- a/docs/topics/db/optimization.txt +++ b/docs/topics/db/optimization.txt @@ -306,6 +306,17 @@ instead of:: entry.blog.id +Don't order results if you don't care +------------------------------------- + +Ordering is not free; each field to order by is an operation the database must +perform. If a model has a default ordering (:attr:`Meta.ordering +<django.db.models.Options.ordering>`) and you don't need it, remove +it on a ``QuerySet`` by calling +:meth:`~django.db.models.query.QuerySet.order_by()` with no parameters. + +Adding an index to your database may help to improve ordering performance. + Insert in bulk ============== From 789d8f07487f80c5b90ec9cbb1fcf99837ef3ace Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Mon, 9 Sep 2013 09:54:08 -0400 Subject: [PATCH 07/58] Fixed syntax error on Python 3.2; refs #20889. --- tests/httpwrappers/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py index 0d9611ef0e..17bb98e24d 100644 --- a/tests/httpwrappers/tests.py +++ b/tests/httpwrappers/tests.py @@ -296,7 +296,7 @@ class HttpResponseTests(unittest.TestCase): h = HttpResponse() f = 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz a\xcc\x88'.encode('latin-1') f = f.decode('utf-8') - h['Content-Disposition'] = u'attachment; filename="%s"' % f + h['Content-Disposition'] = 'attachment; filename="%s"' % f def test_newlines_in_headers(self): # Bug #10188: Do not allow newlines in headers (CR or LF) From fb51c9a0f262eae0ebb36ba1067dc0a4db314401 Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Mon, 9 Sep 2013 11:30:31 -0400 Subject: [PATCH 08/58] Fixed spelling; refs #16895. Thanks Panagiotis Issaris for the report. --- docs/ref/models/options.txt | 2 +- docs/ref/models/querysets.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index 1caeadddf9..b3d6533f9f 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -225,7 +225,7 @@ Django quotes column and table names behind the scenes. Ordering is not a free operation. Each field you add to the ordering incurs a cost to your database. Each foreign key you add will - impliclty include all of its default orderings as well. + implicitly include all of its default orderings as well. ``permissions`` --------------- diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 000415f028..c62525cb72 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -338,7 +338,7 @@ You can tell if a query is ordered or not by checking the Ordering is not a free operation. Each field you add to the ordering incurs a cost to your database. Each foreign key you add will - impliclty include all of its default orderings as well. + implicitly include all of its default orderings as well. reverse ~~~~~~~ From 0d74f9553c6acb8b53a502ca5e39542dcc4412c1 Mon Sep 17 00:00:00 2001 From: Keryn Knight <keryn@kerynknight.com> Date: Sat, 7 Sep 2013 13:32:12 +0100 Subject: [PATCH 09/58] Fixed #21063 -- AdminSite app_index should be fail early if the user has no permissions. --- django/contrib/admin/sites.py | 69 ++++++++++++++++++----------------- tests/admin_views/tests.py | 21 +++++++++++ 2 files changed, 56 insertions(+), 34 deletions(-) diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index b4f45c1e17..bb767a6dfa 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -6,7 +6,7 @@ from django.contrib.auth import logout as auth_logout, REDIRECT_FIELD_NAME from django.contrib.contenttypes import views as contenttype_views from django.views.decorators.csrf import csrf_protect from django.db.models.base import ModelBase -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.core.urlresolvers import reverse, NoReverseMatch from django.template.response import TemplateResponse from django.utils import six @@ -399,44 +399,45 @@ class AdminSite(object): def app_index(self, request, app_label, extra_context=None): user = request.user has_module_perms = user.has_module_perms(app_label) + if not has_module_perms: + raise PermissionDenied app_dict = {} for model, model_admin in self._registry.items(): if app_label == model._meta.app_label: - if has_module_perms: - perms = model_admin.get_model_perms(request) + perms = model_admin.get_model_perms(request) - # Check whether user has any perm for this module. - # If so, add the module to the model_list. - if True in perms.values(): - info = (app_label, model._meta.model_name) - model_dict = { - 'name': capfirst(model._meta.verbose_name_plural), - 'object_name': model._meta.object_name, - 'perms': perms, + # Check whether user has any perm for this module. + # If so, add the module to the model_list. + if True in perms.values(): + info = (app_label, model._meta.model_name) + model_dict = { + 'name': capfirst(model._meta.verbose_name_plural), + 'object_name': model._meta.object_name, + 'perms': perms, + } + if perms.get('change'): + try: + model_dict['admin_url'] = reverse('admin:%s_%s_changelist' % info, current_app=self.name) + except NoReverseMatch: + pass + if perms.get('add'): + try: + model_dict['add_url'] = reverse('admin:%s_%s_add' % info, current_app=self.name) + except NoReverseMatch: + pass + if app_dict: + app_dict['models'].append(model_dict), + else: + # First time around, now that we know there's + # something to display, add in the necessary meta + # information. + app_dict = { + 'name': app_label.title(), + 'app_label': app_label, + 'app_url': '', + 'has_module_perms': has_module_perms, + 'models': [model_dict], } - if perms.get('change', False): - try: - model_dict['admin_url'] = reverse('admin:%s_%s_changelist' % info, current_app=self.name) - except NoReverseMatch: - pass - if perms.get('add', False): - try: - model_dict['add_url'] = reverse('admin:%s_%s_add' % info, current_app=self.name) - except NoReverseMatch: - pass - if app_dict: - app_dict['models'].append(model_dict), - else: - # First time around, now that we know there's - # something to display, add in the necessary meta - # information. - app_dict = { - 'name': app_label.title(), - 'app_label': app_label, - 'app_url': '', - 'has_module_perms': has_module_perms, - 'models': [model_dict], - } if not app_dict: raise Http404('The requested admin page does not exist.') # Sort the models alphabetically within each app. diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index ac289c550a..19bf00f302 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -1309,6 +1309,27 @@ class AdminViewPermissionsTest(TestCase): response = self.client.get('/test_admin/admin/secure-view/') self.assertContains(response, 'id="login-form"') + def testAppIndexFailEarly(self): + """ + If a user has no module perms, avoid iterating over all the modeladmins + in the registry. + """ + opts = Article._meta + change_user = User.objects.get(username='changeuser') + permission = get_perm(Article, get_permission_codename('change', opts)) + + self.client.post('/test_admin/admin/', self.changeuser_login) + + # the user has no module permissions, because this module doesn't exist + change_user.user_permissions.remove(permission) + response = self.client.get('/test_admin/admin/admin_views/') + self.assertEqual(response.status_code, 403) + + # the user now has module permissions + change_user.user_permissions.add(permission) + response = self.client.get('/test_admin/admin/admin_views/') + self.assertEqual(response.status_code, 200) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewsNoUrlTest(TestCase): From 910a5760f6f5ffc42b4b264b08057207b8e26106 Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Mon, 9 Sep 2013 14:22:29 -0400 Subject: [PATCH 10/58] Improved release notes for ticket #10164 Thanks Aymeric for the suggestions. refs #10164 --- docs/releases/1.7.txt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index d6c9dc4b5c..fdba5a80a5 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -345,13 +345,18 @@ Miscellaneous when called on an instance without a primary key value. This is done to avoid mutable ``__hash__`` values in containers. -* The :meth:`django.db.backends.sqlite3.DatabaseCreation.sql_create_model` - will now create :class:`~django.db.models.AutoField` columns in SQLite - databases using the ``AUTOINCREMENT`` option, which guarantees monotonic +* :class:`~django.db.models.AutoField` columns in SQLite databases will now be + created using the ``AUTOINCREMENT`` option, which guarantees monotonic increments. This will cause primary key numbering behavior to change on - SQLite, becoming consistent with most other SQL databases. If you have a - database created with an older version of Django, you will need to migrate - it to take advantage of this feature. See ticket #10164 for details. + SQLite, becoming consistent with most other SQL databases. This will only + apply to newly created tables. If you have a database created with an older + version of Django, you will need to migrate it to take advantage of this + feature. For example, you could do the following: + + #) Use :djadmin:`dumpdata` to save your data. + #) Rename the existing database file (keep it as a backup). + #) Run :djadmin:`migrate` to create the updated schema. + #) Use :djadmin:`loaddata` to import the fixtures you exported in (1). * ``django.contrib.auth.models.AbstractUser`` no longer defines a :meth:`~django.db.models.Model.get_absolute_url()` method. The old definition From a52cc1c0888c2cedb07b2c0619c1a92a2f6e2c40 Mon Sep 17 00:00:00 2001 From: Josh Mize <jgmize@gmail.com> Date: Mon, 9 Sep 2013 11:48:56 -0500 Subject: [PATCH 11/58] Fixed #21078 -- Handled additional bad Accept-Language header --- django/utils/translation/trans_real.py | 5 ++++- tests/i18n/tests.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index a6b7eaa489..c7b41fa373 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -668,7 +668,10 @@ def parse_accept_lang_header(lang_string): if first: return [] if priority: - priority = float(priority) + try: + priority = float(priority) + except ValueError: + return [] if not priority: # if priority is 0.0 at this point make it 1.0 priority = 1.0 result.append((lang, priority)) diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index c18c88cad3..f482dba3bb 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -849,6 +849,7 @@ class MiscTests(TransRealMixin, TestCase): self.assertEqual([], p('de;q=0.a')) self.assertEqual([], p('12-345')) self.assertEqual([], p('')) + self.assertEqual([], p('en; q=1,')) def test_parse_literal_http_header(self): """ From 9d700322b38ea670800a97f2b92dd2fc2c6ff28d Mon Sep 17 00:00:00 2001 From: Kevin Christopher Henry <k@severian.com> Date: Mon, 9 Sep 2013 04:59:47 -0400 Subject: [PATCH 12/58] Fixed #19885 -- cleaned up the django.test namespace * override_settings may now be imported from django.test * removed Approximate from django.test * updated documentation for things importable from django.test Thanks akaariai for the suggestion. --- django/test/__init__.py | 2 +- docs/internals/deprecation.txt | 4 +-- docs/intro/tutorial05.txt | 4 +-- docs/ref/signals.txt | 2 +- docs/releases/1.0-porting-guide.txt | 2 +- docs/releases/1.1-beta-1.txt | 2 +- docs/releases/1.1.txt | 2 -- docs/releases/1.2.2.txt | 2 +- docs/releases/1.3-alpha-1.txt | 6 ++-- docs/releases/1.3.txt | 6 ++-- docs/releases/1.4.6.txt | 2 +- docs/releases/1.5.2.txt | 2 +- docs/releases/1.6.txt | 2 +- docs/topics/auth/customizing.txt | 3 +- docs/topics/testing/advanced.txt | 11 +++----- docs/topics/testing/overview.txt | 43 ++++++++++++----------------- tests/aggregation/tests.py | 3 +- tests/aggregation_regress/tests.py | 3 +- tests/expressions_regress/tests.py | 3 +- tests/serializers/tests.py | 3 +- 20 files changed, 49 insertions(+), 58 deletions(-) diff --git a/django/test/__init__.py b/django/test/__init__.py index 7a4987508e..58d54df5e2 100644 --- a/django/test/__init__.py +++ b/django/test/__init__.py @@ -7,4 +7,4 @@ from django.test.testcases import (TestCase, TransactionTestCase, SimpleTestCase, LiveServerTestCase, skipIfDBFeature, skipUnlessDBFeature ) -from django.test.utils import Approximate +from django.test.utils import override_settings diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 7b8298597d..307f4dec64 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -117,9 +117,9 @@ these changes. * The ``mod_python`` request handler will be removed. The ``mod_wsgi`` handler should be used instead. -* The ``template`` attribute on :class:`~django.test.client.Response` +* The ``template`` attribute on :class:`~django.test.Response` objects returned by the :ref:`test client <test-client>` will be removed. - The :attr:`~django.test.client.Response.templates` attribute should be + The :attr:`~django.test.Response.templates` attribute should be used instead. * The ``django.test.simple.DjangoTestRunner`` will be removed. diff --git a/docs/intro/tutorial05.txt b/docs/intro/tutorial05.txt index 1f0b40ed1d..14a1795a1c 100644 --- a/docs/intro/tutorial05.txt +++ b/docs/intro/tutorial05.txt @@ -319,7 +319,7 @@ Before we try to fix anything, let's have a look at the tools at our disposal. The Django test client ---------------------- -Django provides a test :class:`~django.test.client.Client` to simulate a user +Django provides a test :class:`~django.test.Client` to simulate a user interacting with the code at the view level. We can use it in ``tests.py`` or even in the shell. @@ -341,7 +341,7 @@ Next we need to import the test client class (later in ``tests.py`` we will use the :class:`django.test.TestCase` class, which comes with its own client, so this won't be required):: - >>> from django.test.client import Client + >>> from django.test import Client >>> # create an instance of the client for our use >>> client = Client() diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index 8381a49a09..fed8c7b3b5 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -565,7 +565,7 @@ setting_changed This signal is sent when the value of a setting is changed through the ``django.test.TestCase.settings()`` context manager or the -:func:`django.test.utils.override_settings` decorator/context manager. +:func:`django.test.override_settings` decorator/context manager. It's actually sent twice: when the new value is applied ("setup") and when the original value is restored ("teardown"). Use the ``enter`` argument to diff --git a/docs/releases/1.0-porting-guide.txt b/docs/releases/1.0-porting-guide.txt index 644350525c..5983e365fe 100644 --- a/docs/releases/1.0-porting-guide.txt +++ b/docs/releases/1.0-porting-guide.txt @@ -643,7 +643,7 @@ The generic relation classes -- ``GenericForeignKey`` and ``GenericRelation`` Testing ------- -:meth:`django.test.client.Client.login` has changed +:meth:`django.test.Client.login` has changed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Old (0.96):: diff --git a/docs/releases/1.1-beta-1.txt b/docs/releases/1.1-beta-1.txt index 88d8ce5f35..6d45b9dfc9 100644 --- a/docs/releases/1.1-beta-1.txt +++ b/docs/releases/1.1-beta-1.txt @@ -99,7 +99,7 @@ one fell swoop. Testing improvements -------------------- -.. currentmodule:: django.test.client +.. currentmodule:: django.test A couple of small but very useful improvements have been made to the :doc:`testing framework </topics/testing/index>`: diff --git a/docs/releases/1.1.txt b/docs/releases/1.1.txt index 261b552aff..e3ee9428bf 100644 --- a/docs/releases/1.1.txt +++ b/docs/releases/1.1.txt @@ -285,8 +285,6 @@ full description, and some important notes on database support. Test client improvements ------------------------ -.. currentmodule:: django.test.client - A couple of small -- but highly useful -- improvements have been made to the test client: diff --git a/docs/releases/1.2.2.txt b/docs/releases/1.2.2.txt index 4ae74abbf9..9c3054dc59 100644 --- a/docs/releases/1.2.2.txt +++ b/docs/releases/1.2.2.txt @@ -23,7 +23,7 @@ case of Django 1.2.2, we have made an exception to this rule. In order to test a bug fix that forms part of the 1.2.2 release, it was necessary to add a feature -- the ``enforce_csrf_checks`` flag -- -to the :mod:`test client <django.test.client>`. This flag forces +to the :ref:`test client <test-client>`. This flag forces the test client to perform full CSRF checks on forms. The default behavior of the test client hasn't changed, but if you want to do CSRF checks with the test client, it is now possible to do so. diff --git a/docs/releases/1.3-alpha-1.txt b/docs/releases/1.3-alpha-1.txt index 634e6afaf2..cb629a2cf8 100644 --- a/docs/releases/1.3-alpha-1.txt +++ b/docs/releases/1.3-alpha-1.txt @@ -150,7 +150,7 @@ requests. These include: * Improved tools for accessing and manipulating the current Site via ``django.contrib.sites.models.get_current_site()``. -* A :class:`~django.test.client.RequestFactory` for mocking +* A :class:`~django.test.RequestFactory` for mocking requests in tests. * A new test assertion -- @@ -318,7 +318,7 @@ Test client response ``template`` attribute ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Django's :ref:`test client <test-client>` returns -:class:`~django.test.client.Response` objects annotated with extra testing +:class:`~django.test.Response` objects annotated with extra testing information. In Django versions prior to 1.3, this included a ``template`` attribute containing information about templates rendered in generating the response: either None, a single :class:`~django.template.Template` object, or a @@ -327,7 +327,7 @@ return values (sometimes a list, sometimes not) made the attribute difficult to work with. In Django 1.3 the ``template`` attribute is deprecated in favor of a new -:attr:`~django.test.client.Response.templates` attribute, which is always a +:attr:`~django.test.Response.templates` attribute, which is always a list, even if it has only a single element or no elements. ``DjangoTestRunner`` diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index ecb48803a7..b77668ac3d 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -295,7 +295,7 @@ requests. These include: :class:`~django.contrib.sites.models.Site` object in :doc:`the sites framework </ref/contrib/sites>`. -* A :class:`~django.test.client.RequestFactory` for mocking requests +* A :class:`~django.test.RequestFactory` for mocking requests in tests. * A new test assertion -- @@ -715,7 +715,7 @@ Test client response ``template`` attribute ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Django's :ref:`test client <test-client>` returns -:class:`~django.test.client.Response` objects annotated with extra testing +:class:`~django.test.Response` objects annotated with extra testing information. In Django versions prior to 1.3, this included a ``template`` attribute containing information about templates rendered in generating the response: either None, a single :class:`~django.template.Template` object, or a @@ -724,7 +724,7 @@ return values (sometimes a list, sometimes not) made the attribute difficult to work with. In Django 1.3 the ``template`` attribute is deprecated in favor of a new -:attr:`~django.test.client.Response.templates` attribute, which is always a +:attr:`~django.test.Response.templates` attribute, which is always a list, even if it has only a single element or no elements. ``DjangoTestRunner`` diff --git a/docs/releases/1.4.6.txt b/docs/releases/1.4.6.txt index 575e9fa75a..3847ea26f9 100644 --- a/docs/releases/1.4.6.txt +++ b/docs/releases/1.4.6.txt @@ -26,6 +26,6 @@ header and browsers seem to ignore JavaScript there. Bugfixes ======== -* Fixed an obscure bug with the :func:`~django.test.utils.override_settings` +* Fixed an obscure bug with the :func:`~django.test.override_settings` decorator. If you hit an ``AttributeError: 'Settings' object has no attribute '_original_allowed_hosts'`` exception, it's probably fixed (#20636). diff --git a/docs/releases/1.5.2.txt b/docs/releases/1.5.2.txt index 710f16555c..20771365aa 100644 --- a/docs/releases/1.5.2.txt +++ b/docs/releases/1.5.2.txt @@ -57,6 +57,6 @@ Bugfixes * Ensured that the WSGI request's path is correctly based on the ``SCRIPT_NAME`` environment variable or the :setting:`FORCE_SCRIPT_NAME` setting, regardless of whether or not either has a trailing slash (#20169). -* Fixed an obscure bug with the :func:`~django.test.utils.override_settings` +* Fixed an obscure bug with the :func:`~django.test.override_settings` decorator. If you hit an ``AttributeError: 'Settings' object has no attribute '_original_allowed_hosts'`` exception, it's probably fixed (#20636). diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 72d4df0fee..27c7482535 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -814,7 +814,7 @@ Miscellaneous ``{% url %}`` tag, it causes template rendering to fail like always when ``NoReverseMatch`` is raised. -* :meth:`django.test.client.Client.logout` now calls +* :meth:`django.test.Client.logout` now calls :meth:`django.contrib.auth.logout` which will send the :func:`~django.contrib.auth.signals.user_logged_out` signal. diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index bb44f091fe..1407ed4d8f 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -869,8 +869,7 @@ would test three possible User models -- the default, plus the two User models provided by ``auth`` app:: from django.contrib.auth.tests.utils import skipIfCustomUser - from django.test import TestCase - from django.test.utils import override_settings + from django.test import TestCase, override_settings class ApplicationTestCase(TestCase): diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index dc41747ae4..a4c425aeb9 100644 --- a/docs/topics/testing/advanced.txt +++ b/docs/topics/testing/advanced.txt @@ -5,18 +5,18 @@ Advanced testing topics The request factory =================== -.. module:: django.test.client +.. currentmodule:: django.test .. class:: RequestFactory -The :class:`~django.test.client.RequestFactory` shares the same API as +The :class:`~django.test.RequestFactory` shares the same API as the test client. However, instead of behaving like a browser, the RequestFactory provides a way to generate a request instance that can be used as the first argument to any view. This means you can test a view function the same way as you would test any other function -- as a black box, with exactly known inputs, testing for specific outputs. -The API for the :class:`~django.test.client.RequestFactory` is a slightly +The API for the :class:`~django.test.RequestFactory` is a slightly restricted subset of the test client API: * It only has access to the HTTP methods :meth:`~Client.get()`, @@ -38,8 +38,7 @@ Example The following is a simple unit test using the request factory:: from django.contrib.auth.models import User - from django.test import TestCase - from django.test.client import RequestFactory + from django.test import TestCase, RequestFactory class SimpleTest(TestCase): def setUp(self): @@ -165,8 +164,6 @@ exception will be raised. Advanced features of ``TransactionTestCase`` ============================================ -.. currentmodule:: django.test - .. attribute:: TransactionTestCase.available_apps .. versionadded:: 1.6 diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index f5f8ad24c9..7c6f5caa47 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -313,9 +313,6 @@ Django provides a small set of tools that come in handy when writing tests. The test client --------------- -.. module:: django.test.client - :synopsis: Django's test client. - The test client is a Python class that acts as a dummy Web browser, allowing you to test your views and interact with your Django-powered application programmatically. @@ -349,10 +346,10 @@ A comprehensive test suite should use a combination of both test types. Overview and a quick example ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To use the test client, instantiate ``django.test.client.Client`` and retrieve +To use the test client, instantiate ``django.test.Client`` and retrieve Web pages:: - >>> from django.test.client import Client + >>> from django.test import Client >>> c = Client() >>> response = c.post('/login/', {'username': 'john', 'password': 'smith'}) >>> response.status_code @@ -413,7 +410,7 @@ Note a few important things about how the test client works: Making requests ~~~~~~~~~~~~~~~ -Use the ``django.test.client.Client`` class to make requests. +Use the ``django.test.Client`` class to make requests. .. class:: Client(enforce_csrf_checks=False, **defaults) @@ -424,8 +421,8 @@ Use the ``django.test.client.Client`` class to make requests. >>> c = Client(HTTP_USER_AGENT='Mozilla/5.0') The values from the ``extra`` keywords arguments passed to - :meth:`~django.test.client.Client.get()`, - :meth:`~django.test.client.Client.post()`, etc. have precedence over + :meth:`~django.test.Client.get()`, + :meth:`~django.test.Client.post()`, etc. have precedence over the defaults passed to the class constructor. The ``enforce_csrf_checks`` argument can be used to test CSRF @@ -778,7 +775,7 @@ Example The following is a simple unit test using the test client:: import unittest - from django.test.client import Client + from django.test import Client class SimpleTest(unittest.TestCase): def setUp(self): @@ -797,15 +794,13 @@ The following is a simple unit test using the test client:: .. seealso:: - :class:`django.test.client.RequestFactory` + :class:`django.test.RequestFactory` .. _django-testcase-subclasses: Provided test case classes -------------------------- -.. currentmodule:: django.test - Normal Python unit test classes extend a base class of :class:`unittest.TestCase`. Django provides a few extensions of this base class: @@ -847,7 +842,7 @@ functionality like: for equality. * The ability to run tests with :ref:`modified settings <overriding-settings>`. -* Using the :attr:`~SimpleTestCase.client` :class:`~django.test.client.Client`. +* Using the :attr:`~SimpleTestCase.client` :class:`~django.test.Client`. * Custom test-time :attr:`URL maps <SimpleTestCase.urls>`. .. versionchanged:: 1.6 @@ -1111,7 +1106,7 @@ worry about state (such as cookies) carrying over from one test to another. This means, instead of instantiating a ``Client`` in each test:: import unittest - from django.test.client import Client + from django.test import Client class SimpleTest(unittest.TestCase): def test_details(self): @@ -1146,8 +1141,7 @@ If you want to use a different ``Client`` class (for example, a subclass with customized behavior), use the :attr:`~SimpleTestCase.client_class` class attribute:: - from django.test import TestCase - from django.test.client import Client + from django.test import TestCase, Client class MyTestClient(Client): # Specialized methods for your environment... @@ -1330,17 +1324,14 @@ Django provides a standard Python context manager (see :pep:`343`) This example will override the :setting:`LOGIN_URL` setting for the code in the ``with`` block and reset its value to the previous state afterwards. -.. currentmodule:: django.test.utils - .. function:: override_settings In case you want to override a setting for just one test method or even the whole :class:`~django.test.TestCase` class, Django provides the -:func:`~django.test.utils.override_settings` decorator (see :pep:`318`). It's +:func:`~django.test.override_settings` decorator (see :pep:`318`). It's used like this:: - from django.test import TestCase - from django.test.utils import override_settings + from django.test import TestCase, override_settings class LoginTestCase(TestCase): @@ -1351,8 +1342,7 @@ used like this:: The decorator can also be applied to test case classes:: - from django.test import TestCase - from django.test.utils import override_settings + from django.test import TestCase, override_settings @override_settings(LOGIN_URL='/other/login/') class LoginTestCase(TestCase): @@ -1361,6 +1351,11 @@ The decorator can also be applied to test case classes:: response = self.client.get('/sekrit/') self.assertRedirects(response, '/other/login/?next=/sekrit/') +.. versionchanged:: 1.7 + + Previously, ``override_settings`` was imported from + ``django.test.utils``. + .. note:: When given a class, the decorator modifies the class directly and @@ -1427,8 +1422,6 @@ For more detail on email services during tests, see `Email services`_ below. Assertions ~~~~~~~~~~ -.. currentmodule:: django.test - As Python's normal :class:`unittest.TestCase` class implements assertion methods such as :meth:`~unittest.TestCase.assertTrue` and :meth:`~unittest.TestCase.assertEqual`, Django's custom :class:`TestCase` class diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py index ce7f4e9b9d..5abd4aaf6c 100644 --- a/tests/aggregation/tests.py +++ b/tests/aggregation/tests.py @@ -6,7 +6,8 @@ import re from django.db import connection from django.db.models import Avg, Sum, Count, Max, Min -from django.test import TestCase, Approximate +from django.test import TestCase +from django.test.utils import Approximate from django.test.utils import CaptureQueriesContext from .models import Author, Publisher, Book, Store diff --git a/tests/aggregation_regress/tests.py b/tests/aggregation_regress/tests.py index 8388e78dbe..b8ec14d1dc 100644 --- a/tests/aggregation_regress/tests.py +++ b/tests/aggregation_regress/tests.py @@ -8,7 +8,8 @@ from operator import attrgetter from django.core.exceptions import FieldError from django.contrib.contenttypes.models import ContentType from django.db.models import Count, Max, Avg, Sum, StdDev, Variance, F, Q -from django.test import TestCase, Approximate, skipUnlessDBFeature +from django.test import TestCase, skipUnlessDBFeature +from django.test.utils import Approximate from django.utils import six from .models import (Author, Book, Publisher, Clues, Entries, HardbackBook, diff --git a/tests/expressions_regress/tests.py b/tests/expressions_regress/tests.py index c4f8085804..eb807cb050 100644 --- a/tests/expressions_regress/tests.py +++ b/tests/expressions_regress/tests.py @@ -7,7 +7,8 @@ import datetime from django.db import connection from django.db.models import F -from django.test import TestCase, Approximate, skipUnlessDBFeature +from django.test import TestCase, skipUnlessDBFeature +from django.test.utils import Approximate from .models import Number, Experiment diff --git a/tests/serializers/tests.py b/tests/serializers/tests.py index 458847e4c7..34878adfd2 100644 --- a/tests/serializers/tests.py +++ b/tests/serializers/tests.py @@ -17,7 +17,8 @@ except ImportError: from django.conf import settings from django.core import management, serializers from django.db import transaction, connection -from django.test import TestCase, TransactionTestCase, Approximate +from django.test import TestCase, TransactionTestCase +from django.test.utils import Approximate from django.utils import six from django.utils.six import StringIO From ec2778b445546f624d3b3a1f2118e751b10bb2e7 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin <aymeric.augustin@m4x.org> Date: Sun, 8 Sep 2013 02:04:31 -0500 Subject: [PATCH 13/58] Fixed #17262 -- Refactored tzinfo implementations. This commit deprecates django.utils.tzinfo in favor of the more recent django.utils.timezone which was introduced when Django gained support for time zones. --- django/utils/timezone.py | 60 ++++++++++++++++++++++++------ django/utils/tzinfo.py | 17 ++++++++- docs/internals/deprecation.txt | 2 + docs/ref/utils.txt | 20 ++++++++++ docs/releases/1.7.txt | 19 ++++++++++ tests/timezones/tests.py | 5 +-- tests/utils_tests/test_timezone.py | 5 +-- tests/utils_tests/test_tzinfo.py | 10 ++++- 8 files changed, 117 insertions(+), 21 deletions(-) diff --git a/django/utils/timezone.py b/django/utils/timezone.py index 93ef4dfd57..da6f971c50 100644 --- a/django/utils/timezone.py +++ b/django/utils/timezone.py @@ -18,7 +18,8 @@ from django.conf import settings from django.utils import six __all__ = [ - 'utc', 'get_default_timezone', 'get_current_timezone', + 'utc', 'get_fixed_timezone', + 'get_default_timezone', 'get_current_timezone', 'activate', 'deactivate', 'override', 'is_naive', 'is_aware', 'make_aware', 'make_naive', ] @@ -47,19 +48,45 @@ class UTC(tzinfo): def dst(self, dt): return ZERO +class FixedOffset(tzinfo): + """ + Fixed offset in minutes east from UTC. Taken from Python's docs. + + Kept as close as possible to the reference version. __init__ was changed + to make its arguments optional, according to Python's requirement that + tzinfo subclasses can be instantiated without arguments. + """ + + def __init__(self, offset=None, name=None): + if offset is not None: + self.__offset = timedelta(minutes=offset) + if name is not None: + self.__name = name + + def utcoffset(self, dt): + return self.__offset + + def tzname(self, dt): + return self.__name + + def dst(self, dt): + return ZERO + class ReferenceLocalTimezone(tzinfo): """ - Local time implementation taken from Python's docs. + Local time. Taken from Python's docs. Used only when pytz isn't available, and most likely inaccurate. If you're having trouble with this class, don't waste your time, just install pytz. - Kept identical to the reference version. Subclasses contain improvements. + Kept as close as possible to the reference version. __init__ was added to + delay the computation of STDOFFSET, DSTOFFSET and DSTDIFF which is + performed at import time in the example. + + Subclasses contain further improvements. """ def __init__(self): - # This code is moved in __init__ to execute it as late as possible - # See get_default_timezone(). self.STDOFFSET = timedelta(seconds=-_time.timezone) if _time.daylight: self.DSTOFFSET = timedelta(seconds=-_time.altzone) @@ -68,9 +95,6 @@ class ReferenceLocalTimezone(tzinfo): self.DSTDIFF = self.DSTOFFSET - self.STDOFFSET tzinfo.__init__(self) - def __repr__(self): - return "<LocalTimezone>" - def utcoffset(self, dt): if self._isdst(dt): return self.DSTOFFSET @@ -84,8 +108,7 @@ class ReferenceLocalTimezone(tzinfo): return ZERO def tzname(self, dt): - is_dst = False if dt is None else self._isdst(dt) - return _time.tzname[is_dst] + return _time.tzname[self._isdst(dt)] def _isdst(self, dt): tt = (dt.year, dt.month, dt.day, @@ -103,6 +126,10 @@ class LocalTimezone(ReferenceLocalTimezone): error message is helpful. """ + def tzname(self, dt): + is_dst = False if dt is None else self._isdst(dt) + return _time.tzname[is_dst] + def _isdst(self, dt): try: return super(LocalTimezone, self)._isdst(dt) @@ -116,6 +143,17 @@ class LocalTimezone(ReferenceLocalTimezone): utc = pytz.utc if pytz else UTC() """UTC time zone as a tzinfo instance.""" +def get_fixed_timezone(offset): + """ + Returns a tzinfo instance with a fixed offset from UTC. + """ + if isinstance(offset, timedelta): + offset = offset.seconds // 60 + sign = '-' if offset < 0 else '+' + hhmm = '%02d%02d' % divmod(abs(offset), 60) + name = sign + hhmm + return FixedOffset(offset, name) + # In order to avoid accessing the settings at compile time, # wrap the expression in a function and cache the result. _localtime = None @@ -125,8 +163,6 @@ def get_default_timezone(): Returns the default time zone as a tzinfo instance. This is the time zone defined by settings.TIME_ZONE. - - See also :func:`get_current_timezone`. """ global _localtime if _localtime is None: diff --git a/django/utils/tzinfo.py b/django/utils/tzinfo.py index fd221ea48b..62c8e07a66 100644 --- a/django/utils/tzinfo.py +++ b/django/utils/tzinfo.py @@ -2,11 +2,18 @@ from __future__ import unicode_literals -import time from datetime import timedelta, tzinfo +import time +import warnings from django.utils.encoding import force_str, force_text, DEFAULT_LOCALE_ENCODING +warnings.warn( + "django.utils.tzinfo will be removed in Django 1.9. " + "Use django.utils.timezone instead.", + PendingDeprecationWarning) + + # Python's doc say: "A tzinfo subclass must have an __init__() method that can # be called with no arguments". FixedOffset and LocalTimezone don't honor this # requirement. Defining __getinitargs__ is sufficient to fix copy/deepcopy as @@ -15,6 +22,10 @@ from django.utils.encoding import force_str, force_text, DEFAULT_LOCALE_ENCODING class FixedOffset(tzinfo): "Fixed offset in minutes east from UTC." def __init__(self, offset): + warnings.warn( + "django.utils.tzinfo.FixedOffset will be removed in Django 1.9. " + "Use django.utils.timezone.get_fixed_timezone instead.", + PendingDeprecationWarning) if isinstance(offset, timedelta): self.__offset = offset offset = self.__offset.seconds // 60 @@ -48,6 +59,10 @@ class FixedOffset(tzinfo): class LocalTimezone(tzinfo): "Proxy timezone information from time module." def __init__(self, dt): + warnings.warn( + "django.utils.tzinfo.LocalTimezone will be removed in Django 1.9. " + "Use django.utils.timezone.get_default_timezone instead.", + PendingDeprecationWarning) tzinfo.__init__(self) self.__dt = dt self._tzname = self.tzname(dt) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 307f4dec64..054e3b32fc 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -417,6 +417,8 @@ these changes. * ``django.utils.importlib`` will be removed. +* ``django.utils.tzinfo`` will be removed. + * ``django.utils.unittest`` will be removed. * The ``syncdb`` command will be removed. diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index a093de18b7..59a501cf82 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -927,6 +927,17 @@ For a complete discussion on the usage of the following see the :class:`~datetime.tzinfo` instance that represents UTC. +.. function:: get_fixed_timezone(offset) + + .. versionadded:: 1.7 + + Returns a :class:`~datetime.tzinfo` instance that represents a time zone + with a fixed offset from UTC. + + ``offset`` is a :class:`datetime.timedelta` or an integer number of + minutes. Use positive values for time zones east of UTC and negative + values for west of UTC. + .. function:: get_default_timezone() Returns a :class:`~datetime.tzinfo` instance that represents the @@ -1021,6 +1032,9 @@ For a complete discussion on the usage of the following see the ``django.utils.tzinfo`` ======================= +.. deprecated:: 1.7 + Use :mod:`~django.utils.timezone` instead. + .. module:: django.utils.tzinfo :synopsis: Implementation of ``tzinfo`` classes for use with ``datetime.datetime``. @@ -1028,6 +1042,12 @@ For a complete discussion on the usage of the following see the Fixed offset in minutes east from UTC. + .. deprecated:: 1.7 + Use :func:`~django.utils.timezone.get_fixed_timezone` instead. + .. class:: LocalTimezone Proxy timezone information from time module. + + .. deprecated:: 1.7 + Use :func:`~django.utils.timezone.get_default_timezone` instead. diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index fdba5a80a5..c49ad86c6c 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -308,6 +308,16 @@ For apps with migrations, ``allow_migrate`` will now get passed without custom attributes, methods or managers. Make sure your ``allow_migrate`` methods are only referring to fields or other items in ``model._meta``. +pytz may be required +~~~~~~~~~~~~~~~~~~~~ + +If your project handles datetimes before 1970 or after 2037 and Django raises +a :exc:`~exceptions.ValueError` when encountering them, you will have to +install pytz_. You may be affected by this problem if you use Django's time +zone-related date formats or :mod:`django.contrib.syndication`. + +.. _pytz: https://pypi.python.org/pypi/pytz/ + Miscellaneous ~~~~~~~~~~~~~ @@ -389,6 +399,15 @@ Features deprecated in 1.7 respectively :mod:`logging.config` and :mod:`importlib` provided for Python versions prior to 2.7. They have been deprecated. +``django.utils.tzinfo`` +~~~~~~~~~~~~~~~~~~~~~~~ + +``django.utils.tzinfo`` provided two :class:`~datetime.tzinfo` subclasses, +``LocalTimezone`` and ``FixedOffset``. They've been deprecated in favor of +more correct alternatives provided by :mod:`django.utils.timezone`, +:func:`django.utils.timezone.get_default_timezone` and +:func:`django.utils.timezone.get_fixed_timezone`. + ``django.utils.unittest`` ~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/timezones/tests.py b/tests/timezones/tests.py index 9390eb93df..e9d29f704b 100644 --- a/tests/timezones/tests.py +++ b/tests/timezones/tests.py @@ -25,7 +25,6 @@ from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature from django.test.utils import override_settings from django.utils import six from django.utils import timezone -from django.utils.tzinfo import FixedOffset from .forms import EventForm, EventSplitForm, EventModelForm from .models import Event, MaybeEvent, Session, SessionEvent, Timestamp, AllDayEvent @@ -40,8 +39,8 @@ from .models import Event, MaybeEvent, Session, SessionEvent, Timestamp, AllDayE # 10:20:30 in UTC and 17:20:30 in ICT. UTC = timezone.utc -EAT = FixedOffset(180) # Africa/Nairobi -ICT = FixedOffset(420) # Asia/Bangkok +EAT = timezone.get_fixed_timezone(180) # Africa/Nairobi +ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok TZ_SUPPORT = hasattr(time, 'tzset') diff --git a/tests/utils_tests/test_timezone.py b/tests/utils_tests/test_timezone.py index 6d5f9a7482..de80afe325 100644 --- a/tests/utils_tests/test_timezone.py +++ b/tests/utils_tests/test_timezone.py @@ -6,11 +6,10 @@ import unittest from django.test.utils import override_settings from django.utils import six from django.utils import timezone -from django.utils.tzinfo import FixedOffset -EAT = FixedOffset(180) # Africa/Nairobi -ICT = FixedOffset(420) # Asia/Bangkok +EAT = timezone.get_fixed_timezone(180) # Africa/Nairobi +ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok class TimezoneTests(unittest.TestCase): diff --git a/tests/utils_tests/test_tzinfo.py b/tests/utils_tests/test_tzinfo.py index 1867743fef..31fd7c4a50 100644 --- a/tests/utils_tests/test_tzinfo.py +++ b/tests/utils_tests/test_tzinfo.py @@ -4,10 +4,16 @@ import os import pickle import time import unittest +import warnings -from django.utils.tzinfo import FixedOffset, LocalTimezone +from django.test.utils import IgnorePendingDeprecationWarningsMixin -class TzinfoTests(unittest.TestCase): +# Swallow the import-time warning to test the deprecated implementation. +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=PendingDeprecationWarning) + from django.utils.tzinfo import FixedOffset, LocalTimezone + +class TzinfoTests(IgnorePendingDeprecationWarningsMixin, unittest.TestCase): @classmethod def setUpClass(cls): From d9413d33b2a8371731a92289123683cf6f440290 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin <aymeric.augustin@m4x.org> Date: Sun, 8 Sep 2013 10:43:33 +0200 Subject: [PATCH 14/58] Refactored code and tests that relied on django.utils.tzinfo. Refs #17262. --- django/contrib/humanize/tests.py | 7 ++--- django/contrib/syndication/views.py | 12 ++++---- django/utils/dateformat.py | 5 ++-- django/utils/dateparse.py | 9 +++--- tests/model_regress/tests.py | 6 ++-- tests/syndication/feeds.py | 5 ++-- tests/syndication/tests.py | 34 ++++++++++++++------- tests/template_tests/filters.py | 12 ++++---- tests/utils_tests/test_dateformat.py | 40 ++++++++----------------- tests/utils_tests/test_dateparse.py | 8 ++--- tests/utils_tests/test_feedgenerator.py | 6 ++-- tests/utils_tests/test_timesince.py | 7 +++-- 12 files changed, 75 insertions(+), 76 deletions(-) diff --git a/django/contrib/humanize/tests.py b/django/contrib/humanize/tests.py index f41c948305..7997986370 100644 --- a/django/contrib/humanize/tests.py +++ b/django/contrib/humanize/tests.py @@ -14,10 +14,9 @@ from django.template import Template, Context, defaultfilters from django.test import TestCase from django.test.utils import override_settings from django.utils.html import escape -from django.utils.timezone import utc +from django.utils.timezone import utc, get_fixed_timezone from django.utils import translation from django.utils.translation import ugettext as _ -from django.utils import tzinfo from i18n import TransRealMixin @@ -153,8 +152,8 @@ class HumanizeTests(TransRealMixin, TestCase): def test_naturalday_tz(self): today = datetime.date.today() - tz_one = tzinfo.FixedOffset(datetime.timedelta(hours=-12)) - tz_two = tzinfo.FixedOffset(datetime.timedelta(hours=12)) + tz_one = get_fixed_timezone(-720) + tz_two = get_fixed_timezone(720) # Can be today or yesterday date_one = datetime.datetime(today.year, today.month, today.day, tzinfo=tz_one) diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py index cec204dc92..9878070d4b 100644 --- a/django/contrib/syndication/views.py +++ b/django/contrib/syndication/views.py @@ -7,12 +7,12 @@ from django.contrib.sites.models import get_current_site from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.http import HttpResponse, Http404 from django.template import loader, TemplateDoesNotExist, RequestContext -from django.utils import feedgenerator, tzinfo +from django.utils import feedgenerator from django.utils.encoding import force_text, iri_to_uri, smart_text from django.utils.html import escape from django.utils.http import http_date from django.utils import six -from django.utils.timezone import is_naive +from django.utils.timezone import get_default_timezone, is_naive, make_aware def add_domain(domain, url, secure=False): @@ -186,15 +186,15 @@ class Feed(object): else: author_email = author_link = None + tz = get_default_timezone() + pubdate = self.__get_dynamic_attr('item_pubdate', item) if pubdate and is_naive(pubdate): - ltz = tzinfo.LocalTimezone(pubdate) - pubdate = pubdate.replace(tzinfo=ltz) + pubdate = make_aware(pubdate, tz) updateddate = self.__get_dynamic_attr('item_updateddate', item) if updateddate and is_naive(updateddate): - ltz = tzinfo.LocalTimezone(updateddate) - updateddate = updateddate.replace(tzinfo=ltz) + updateddate = make_aware(updateddate, tz) feed.add_item( title = title, diff --git a/django/utils/dateformat.py b/django/utils/dateformat.py index fbf299631b..85eb975f84 100644 --- a/django/utils/dateformat.py +++ b/django/utils/dateformat.py @@ -18,11 +18,10 @@ import calendar import datetime from django.utils.dates import MONTHS, MONTHS_3, MONTHS_ALT, MONTHS_AP, WEEKDAYS, WEEKDAYS_ABBR -from django.utils.tzinfo import LocalTimezone from django.utils.translation import ugettext as _ from django.utils.encoding import force_text from django.utils import six -from django.utils.timezone import is_aware, is_naive +from django.utils.timezone import get_default_timezone, is_aware, is_naive re_formatchars = re.compile(r'(?<!\\)([aAbBcdDeEfFgGhHiIjlLmMnNoOPrsStTUuwWyYzZ])') re_escaped = re.compile(r'\\(.)') @@ -48,7 +47,7 @@ class TimeFormat(Formatter): # or time objects (against established django policy). if isinstance(obj, datetime.datetime): if is_naive(obj): - self.timezone = LocalTimezone(obj) + self.timezone = get_default_timezone() else: self.timezone = obj.tzinfo diff --git a/django/utils/dateparse.py b/django/utils/dateparse.py index b4bd559a66..1e2cad7d01 100644 --- a/django/utils/dateparse.py +++ b/django/utils/dateparse.py @@ -8,8 +8,8 @@ import datetime import re from django.utils import six -from django.utils.timezone import utc -from django.utils.tzinfo import FixedOffset +from django.utils.timezone import utc, get_fixed_timezone + date_re = re.compile( r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$' @@ -27,6 +27,7 @@ datetime_re = re.compile( r'(?P<tzinfo>Z|[+-]\d{2}:?\d{2})?$' ) + def parse_date(value): """Parses a string and return a datetime.date. @@ -59,7 +60,7 @@ def parse_datetime(value): """Parses a string and return a datetime.datetime. This function supports time zone offsets. When the input contains one, - the output uses an instance of FixedOffset as tzinfo. + the output uses a timezone with a fixed offset from UTC. Raises ValueError if the input is well formatted but not a valid datetime. Returns None if the input isn't well formatted. @@ -76,7 +77,7 @@ def parse_datetime(value): offset = 60 * int(tzinfo[1:3]) + int(tzinfo[-2:]) if tzinfo[0] == '-': offset = -offset - tzinfo = FixedOffset(offset) + tzinfo = get_fixed_timezone(offset) kw = dict((k, int(v)) for k, v in six.iteritems(kw) if v is not None) kw['tzinfo'] = tzinfo return datetime.datetime(**kw) diff --git a/tests/model_regress/tests.py b/tests/model_regress/tests.py index f84a40b05b..a0293975aa 100644 --- a/tests/model_regress/tests.py +++ b/tests/model_regress/tests.py @@ -8,7 +8,7 @@ import unittest from django.core.exceptions import ValidationError from django.test import TestCase, skipUnlessDBFeature from django.utils import six -from django.utils import tzinfo +from django.utils.timezone import get_fixed_timezone from django.db import connection, router from django.db.models.sql import InsertQuery @@ -189,8 +189,8 @@ class ModelTests(TestCase): # Regression test for #10443. # The idea is that all these creations and saving should work without # crashing. It's not rocket science. - dt1 = datetime.datetime(2008, 8, 31, 16, 20, tzinfo=tzinfo.FixedOffset(600)) - dt2 = datetime.datetime(2008, 8, 31, 17, 20, tzinfo=tzinfo.FixedOffset(600)) + dt1 = datetime.datetime(2008, 8, 31, 16, 20, tzinfo=get_fixed_timezone(600)) + dt2 = datetime.datetime(2008, 8, 31, 17, 20, tzinfo=get_fixed_timezone(600)) obj = Article.objects.create( headline="A headline", pub_date=dt1, article_text="foo" ) diff --git a/tests/syndication/feeds.py b/tests/syndication/feeds.py index f8ffb4b2e6..2097cb62ad 100644 --- a/tests/syndication/feeds.py +++ b/tests/syndication/feeds.py @@ -2,7 +2,8 @@ from __future__ import unicode_literals from django.contrib.syndication import views from django.core.exceptions import ObjectDoesNotExist -from django.utils import feedgenerator, tzinfo +from django.utils import feedgenerator +from django.utils.timezone import get_fixed_timezone from .models import Article, Entry @@ -140,7 +141,7 @@ class TZAwareDatesFeed(TestAtomFeed): # Provide a weird offset so that the test can know it's getting this # specific offset and not accidentally getting on from # settings.TIME_ZONE. - return item.published.replace(tzinfo=tzinfo.FixedOffset(42)) + return item.published.replace(tzinfo=get_fixed_timezone(42)) class TestFeedUrlFeed(TestAtomFeed): diff --git a/tests/syndication/tests.py b/tests/syndication/tests.py index 79004aa8b9..92b844fc25 100644 --- a/tests/syndication/tests.py +++ b/tests/syndication/tests.py @@ -1,19 +1,36 @@ from __future__ import unicode_literals +import datetime from xml.dom import minidom +try: + import pytz +except ImportError: + pytz = None + from django.contrib.syndication import views from django.core.exceptions import ImproperlyConfigured from django.test import TestCase -from django.utils import tzinfo from django.utils.feedgenerator import rfc2822_date, rfc3339_date +from django.utils import timezone from .models import Entry +TZ = timezone.get_default_timezone() + + class FeedTestCase(TestCase): fixtures = ['feeddata.json'] + def setUp(self): + # Django cannot deal with very old dates when pytz isn't installed. + if pytz is None: + old_entry = Entry.objects.get(pk=1) + old_entry.updated = datetime.datetime(1980, 1, 1, 12, 30) + old_entry.published = datetime.datetime(1986, 9, 25, 20, 15, 00) + old_entry.save() + def assertChildNodes(self, elem, expected): actual = set(n.nodeName for n in elem.childNodes) expected = set(expected) @@ -59,8 +76,7 @@ class SyndicationFeedTest(FeedTestCase): # Find the last build date d = Entry.objects.latest('published').published - ltz = tzinfo.LocalTimezone(d) - last_build_date = rfc2822_date(d.replace(tzinfo=ltz)) + last_build_date = rfc2822_date(timezone.make_aware(d, TZ)) self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link', 'ttl', 'copyright', 'category']) self.assertChildNodeContent(chan, { @@ -89,8 +105,7 @@ class SyndicationFeedTest(FeedTestCase): # Find the pubdate of the first feed item d = Entry.objects.get(pk=1).published - ltz = tzinfo.LocalTimezone(d) - pub_date = rfc2822_date(d.replace(tzinfo=ltz)) + pub_date = rfc2822_date(timezone.make_aware(d, TZ)) items = chan.getElementsByTagName('item') self.assertEqual(len(items), Entry.objects.count()) @@ -242,8 +257,7 @@ class SyndicationFeedTest(FeedTestCase): updated = feed.getElementsByTagName('updated')[0].firstChild.wholeText d = Entry.objects.latest('published').published - ltz = tzinfo.LocalTimezone(d) - latest_published = rfc3339_date(d.replace(tzinfo=ltz)) + latest_published = rfc3339_date(timezone.make_aware(d, TZ)) self.assertEqual(updated, latest_published) @@ -253,8 +267,7 @@ class SyndicationFeedTest(FeedTestCase): updated = feed.getElementsByTagName('updated')[0].firstChild.wholeText d = Entry.objects.exclude(pk=5).latest('updated').updated - ltz = tzinfo.LocalTimezone(d) - latest_updated = rfc3339_date(d.replace(tzinfo=ltz)) + latest_updated = rfc3339_date(timezone.make_aware(d, TZ)) self.assertEqual(updated, latest_updated) @@ -308,8 +321,7 @@ class SyndicationFeedTest(FeedTestCase): updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText d = Entry.objects.latest('published').published - ltz = tzinfo.LocalTimezone(d) - latest = rfc3339_date(d.replace(tzinfo=ltz)) + latest = rfc3339_date(timezone.make_aware(d, TZ)) self.assertEqual(updated, latest) diff --git a/tests/template_tests/filters.py b/tests/template_tests/filters.py index 142f56f073..317b8bcd23 100644 --- a/tests/template_tests/filters.py +++ b/tests/template_tests/filters.py @@ -11,9 +11,9 @@ from __future__ import unicode_literals from datetime import date, datetime, time, timedelta from django.test.utils import str_prefix -from django.utils.tzinfo import LocalTimezone, FixedOffset -from django.utils.safestring import mark_safe from django.utils.encoding import python_2_unicode_compatible +from django.utils.safestring import mark_safe +from django.utils import timezone # These two classes are used to test auto-escaping of __unicode__ output. @python_2_unicode_compatible @@ -31,8 +31,8 @@ class SafeClass: # 'expected string output' or Exception class) def get_filter_tests(): now = datetime.now() - now_tz = datetime.now(LocalTimezone(now)) - now_tz_i = datetime.now(FixedOffset((3 * 60) + 15)) # imaginary time zone + now_tz = timezone.make_aware(now, timezone.get_default_timezone()) + now_tz_i = timezone.localtime(now_tz, timezone.get_fixed_timezone(195)) today = date.today() # NOTE: \xa0 avoids wrapping between value and unit @@ -355,7 +355,7 @@ def get_filter_tests(): 'date04': (r'{{ d|date:"o" }}', {'d': datetime(2008, 12, 29)}, '2009'), 'date05': (r'{{ d|date:"o" }}', {'d': datetime(2010, 1, 3)}, '2009'), # Timezone name - 'date06': (r'{{ d|date:"e" }}', {'d': datetime(2009, 3, 12, tzinfo=FixedOffset(30))}, '+0030'), + 'date06': (r'{{ d|date:"e" }}', {'d': datetime(2009, 3, 12, tzinfo=timezone.get_fixed_timezone(30))}, '+0030'), 'date07': (r'{{ d|date:"e" }}', {'d': datetime(2009, 3, 12)}, ''), # Ticket 19370: Make sure |date doesn't blow up on a midnight time object 'date08': (r'{{ t|date:"H:i" }}', {'t': time(0, 1)}, '00:01'), @@ -363,7 +363,7 @@ def get_filter_tests(): # Ticket 20693: Add timezone support to built-in time template filter 'time01': (r'{{ dt|time:"e:O:T:Z" }}', {'dt': now_tz_i}, '+0315:+0315:+0315:11700'), 'time02': (r'{{ dt|time:"e:T" }}', {'dt': now}, ':' + now_tz.tzinfo.tzname(now_tz)), - 'time03': (r'{{ t|time:"P:e:O:T:Z" }}', {'t': time(4, 0, tzinfo=FixedOffset(30))}, '4 a.m.::::'), + 'time03': (r'{{ t|time:"P:e:O:T:Z" }}', {'t': time(4, 0, tzinfo=timezone.get_fixed_timezone(30))}, '4 a.m.::::'), 'time04': (r'{{ t|time:"P:e:O:T:Z" }}', {'t': time(4, 0)}, '4 a.m.::::'), 'time05': (r'{{ d|time:"P:e:O:T:Z" }}', {'d': today}, ''), 'time06': (r'{{ obj|time:"P:e:O:T:Z" }}', {'obj': 'non-datetime-value'}, ''), diff --git a/tests/utils_tests/test_dateformat.py b/tests/utils_tests/test_dateformat.py index 2f682b6ce0..a1980b7930 100644 --- a/tests/utils_tests/test_dateformat.py +++ b/tests/utils_tests/test_dateformat.py @@ -1,42 +1,28 @@ from __future__ import unicode_literals from datetime import datetime, date -import os import time -import unittest +from django.test import TestCase +from django.test.utils import override_settings from django.utils.dateformat import format from django.utils import dateformat +from django.utils.timezone import utc, get_fixed_timezone, get_default_timezone from django.utils import translation -from django.utils.timezone import utc -from django.utils.tzinfo import FixedOffset, LocalTimezone -class DateFormatTests(unittest.TestCase): +@override_settings(TIME_ZONE='Europe/Copenhagen') +class DateFormatTests(TestCase): + + # Run tests that require a time zone only when the OS supports it. + tz_tests = hasattr(time, 'tzset') + def setUp(self): - self.old_TZ = os.environ.get('TZ') - os.environ['TZ'] = 'Europe/Copenhagen' self._orig_lang = translation.get_language() translation.activate('en-us') - try: - # Check if a timezone has been set - time.tzset() - self.tz_tests = True - except AttributeError: - # No timezone available. Don't run the tests that require a TZ - self.tz_tests = False - def tearDown(self): translation.activate(self._orig_lang) - if self.old_TZ is None: - del os.environ['TZ'] - else: - os.environ['TZ'] = self.old_TZ - - # Cleanup - force re-evaluation of TZ environment variable. - if self.tz_tests: - time.tzset() def test_date(self): d = date(2009, 5, 16) @@ -47,14 +33,14 @@ class DateFormatTests(unittest.TestCase): self.assertEqual(datetime.fromtimestamp(int(format(dt, 'U'))), dt) def test_datetime_with_local_tzinfo(self): - ltz = LocalTimezone(datetime.now()) + ltz = get_default_timezone() dt = datetime(2009, 5, 16, 5, 30, 30, tzinfo=ltz) self.assertEqual(datetime.fromtimestamp(int(format(dt, 'U')), ltz), dt) self.assertEqual(datetime.fromtimestamp(int(format(dt, 'U'))), dt.replace(tzinfo=None)) def test_datetime_with_tzinfo(self): - tz = FixedOffset(-510) - ltz = LocalTimezone(datetime.now()) + tz = get_fixed_timezone(-510) + ltz = get_default_timezone() dt = datetime(2009, 5, 16, 5, 30, 30, tzinfo=tz) self.assertEqual(datetime.fromtimestamp(int(format(dt, 'U')), tz), dt) self.assertEqual(datetime.fromtimestamp(int(format(dt, 'U')), ltz), dt) @@ -128,7 +114,7 @@ class DateFormatTests(unittest.TestCase): timestamp = datetime(2008, 5, 19, 11, 45, 23, 123456) # 3h30m to the west of UTC - tz = FixedOffset(-3*60 - 30) + tz = get_fixed_timezone(-210) aware_dt = datetime(2009, 5, 16, 5, 30, 30, tzinfo=tz) if self.tz_tests: diff --git a/tests/utils_tests/test_dateparse.py b/tests/utils_tests/test_dateparse.py index 836c26c573..61afff9a9c 100644 --- a/tests/utils_tests/test_dateparse.py +++ b/tests/utils_tests/test_dateparse.py @@ -4,7 +4,7 @@ from datetime import date, time, datetime import unittest from django.utils.dateparse import parse_date, parse_time, parse_datetime -from django.utils.tzinfo import FixedOffset +from django.utils.timezone import get_fixed_timezone class DateParseTests(unittest.TestCase): @@ -34,11 +34,11 @@ class DateParseTests(unittest.TestCase): self.assertEqual(parse_datetime('2012-4-9 4:8:16'), datetime(2012, 4, 9, 4, 8, 16)) self.assertEqual(parse_datetime('2012-04-23T09:15:00Z'), - datetime(2012, 4, 23, 9, 15, 0, 0, FixedOffset(0))) + datetime(2012, 4, 23, 9, 15, 0, 0, get_fixed_timezone(0))) self.assertEqual(parse_datetime('2012-4-9 4:8:16-0320'), - datetime(2012, 4, 9, 4, 8, 16, 0, FixedOffset(-200))) + datetime(2012, 4, 9, 4, 8, 16, 0, get_fixed_timezone(-200))) self.assertEqual(parse_datetime('2012-04-23T10:20:30.400+02:30'), - datetime(2012, 4, 23, 10, 20, 30, 400000, FixedOffset(150))) + datetime(2012, 4, 23, 10, 20, 30, 400000, get_fixed_timezone(150))) # Invalid inputs self.assertEqual(parse_datetime('20120423091500'), None) self.assertRaises(ValueError, parse_datetime, '2012-04-56T09:15:90') diff --git a/tests/utils_tests/test_feedgenerator.py b/tests/utils_tests/test_feedgenerator.py index a801305f17..99d9f5ff7d 100644 --- a/tests/utils_tests/test_feedgenerator.py +++ b/tests/utils_tests/test_feedgenerator.py @@ -4,7 +4,7 @@ import datetime import unittest from django.utils import feedgenerator -from django.utils import tzinfo +from django.utils.timezone import get_fixed_timezone class FeedgeneratorTest(unittest.TestCase): @@ -43,7 +43,7 @@ class FeedgeneratorTest(unittest.TestCase): Test rfc2822_date() correctly formats datetime objects with tzinfo. """ self.assertEqual( - feedgenerator.rfc2822_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=tzinfo.FixedOffset(datetime.timedelta(minutes=60)))), + feedgenerator.rfc2822_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=get_fixed_timezone(60))), "Fri, 14 Nov 2008 13:37:00 +0100" ) @@ -70,7 +70,7 @@ class FeedgeneratorTest(unittest.TestCase): Test rfc3339_date() correctly formats datetime objects with tzinfo. """ self.assertEqual( - feedgenerator.rfc3339_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=tzinfo.FixedOffset(datetime.timedelta(minutes=120)))), + feedgenerator.rfc3339_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=get_fixed_timezone(120))), "2008-11-14T13:37:00+02:00" ) diff --git a/tests/utils_tests/test_timesince.py b/tests/utils_tests/test_timesince.py index cdb95e6877..b454131b2d 100644 --- a/tests/utils_tests/test_timesince.py +++ b/tests/utils_tests/test_timesince.py @@ -4,7 +4,8 @@ import datetime import unittest from django.utils.timesince import timesince, timeuntil -from django.utils.tzinfo import LocalTimezone, FixedOffset +from django.utils import timezone + class TimesinceTests(unittest.TestCase): @@ -95,8 +96,8 @@ class TimesinceTests(unittest.TestCase): def test_different_timezones(self): """ When using two different timezones. """ now = datetime.datetime.now() - now_tz = datetime.datetime.now(LocalTimezone(now)) - now_tz_i = datetime.datetime.now(FixedOffset((3 * 60) + 15)) + now_tz = timezone.make_aware(now, timezone.get_default_timezone()) + now_tz_i = timezone.localtime(now_tz, timezone.get_fixed_timezone(195)) self.assertEqual(timesince(now), '0\xa0minutes') self.assertEqual(timesince(now_tz), '0\xa0minutes') From df2fd4e09b1d51e5154e607babf232610a0ec0c4 Mon Sep 17 00:00:00 2001 From: Florian Apolloner <florian@apolloner.eu> Date: Mon, 9 Sep 2013 22:44:38 +0200 Subject: [PATCH 15/58] Removed unneeded imports in tests's __init__.py and unified them. --- tests/admin_custom_urls/__init__.py | 1 - tests/bash_completion/__init__.py | 1 - tests/bash_completion/management/__init__.py | 1 - .../management/commands/__init__.py | 1 - tests/comment_tests/tests/__init__.py | 8 -------- tests/conditional_processing/__init__.py | 1 - tests/delete_regress/__init__.py | 1 - tests/dispatch/__init__.py | 2 -- tests/dispatch/tests/__init__.py | 6 ------ tests/distinct_on_fields/__init__.py | 1 - tests/file_storage/__init__.py | 1 - tests/files/__init__.py | 1 - tests/fixtures/__init__.py | 2 -- tests/fixtures_model_package/__init__.py | 2 -- tests/forms_tests/tests/__init__.py | 19 ------------------- tests/inspectdb/__init__.py | 1 - tests/invalid_models/__init__.py | 1 - tests/m2m_signals/__init__.py | 1 - tests/m2m_through/__init__.py | 2 -- tests/m2m_through_regress/__init__.py | 2 -- tests/mail/__init__.py | 2 -- tests/max_lengths/__init__.py | 1 - tests/model_package/__init__.py | 1 - tests/requests/__init__.py | 3 --- tests/select_for_update/__init__.py | 1 - tests/unmanaged_models/__init__.py | 2 -- tests/view_tests/app0/__init__.py | 1 - tests/view_tests/app1/__init__.py | 1 - tests/view_tests/app2/__init__.py | 1 - tests/view_tests/app3/__init__.py | 1 - tests/view_tests/app4/__init__.py | 1 - tests/view_tests/tests/__init__.py | 8 -------- 32 files changed, 78 deletions(-) diff --git a/tests/admin_custom_urls/__init__.py b/tests/admin_custom_urls/__init__.py index 792d600548..e69de29bb2 100644 --- a/tests/admin_custom_urls/__init__.py +++ b/tests/admin_custom_urls/__init__.py @@ -1 +0,0 @@ -# diff --git a/tests/bash_completion/__init__.py b/tests/bash_completion/__init__.py index 8b13789179..e69de29bb2 100644 --- a/tests/bash_completion/__init__.py +++ b/tests/bash_completion/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/bash_completion/management/__init__.py b/tests/bash_completion/management/__init__.py index 8b13789179..e69de29bb2 100644 --- a/tests/bash_completion/management/__init__.py +++ b/tests/bash_completion/management/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/bash_completion/management/commands/__init__.py b/tests/bash_completion/management/commands/__init__.py index 8b13789179..e69de29bb2 100644 --- a/tests/bash_completion/management/commands/__init__.py +++ b/tests/bash_completion/management/commands/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/comment_tests/tests/__init__.py b/tests/comment_tests/tests/__init__.py index 6cbddbe82b..df6d60010f 100644 --- a/tests/comment_tests/tests/__init__.py +++ b/tests/comment_tests/tests/__init__.py @@ -83,11 +83,3 @@ class CommentTestCase(TestCase): d.update(f.initial) return d -from comment_tests.tests.test_app_api import * -from comment_tests.tests.test_feeds import * -from comment_tests.tests.test_models import * -from comment_tests.tests.test_comment_form import * -from comment_tests.tests.test_templatetags import * -from comment_tests.tests.test_comment_view import * -from comment_tests.tests.test_comment_utils_moderators import * -from comment_tests.tests.test_moderation_views import * diff --git a/tests/conditional_processing/__init__.py b/tests/conditional_processing/__init__.py index 380474e035..e69de29bb2 100644 --- a/tests/conditional_processing/__init__.py +++ b/tests/conditional_processing/__init__.py @@ -1 +0,0 @@ -# -*- coding:utf-8 -*- diff --git a/tests/delete_regress/__init__.py b/tests/delete_regress/__init__.py index 8b13789179..e69de29bb2 100644 --- a/tests/delete_regress/__init__.py +++ b/tests/delete_regress/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/dispatch/__init__.py b/tests/dispatch/__init__.py index 679895bb5c..e69de29bb2 100644 --- a/tests/dispatch/__init__.py +++ b/tests/dispatch/__init__.py @@ -1,2 +0,0 @@ -"""Unit-tests for the dispatch project -""" diff --git a/tests/dispatch/tests/__init__.py b/tests/dispatch/tests/__init__.py index 4c4d4fea8a..e69de29bb2 100644 --- a/tests/dispatch/tests/__init__.py +++ b/tests/dispatch/tests/__init__.py @@ -1,6 +0,0 @@ -""" -Unit-tests for the dispatch project -""" - -from .test_dispatcher import DispatcherTests, ReceiverTestCase -from .test_saferef import SaferefTests diff --git a/tests/distinct_on_fields/__init__.py b/tests/distinct_on_fields/__init__.py index 792d600548..e69de29bb2 100644 --- a/tests/distinct_on_fields/__init__.py +++ b/tests/distinct_on_fields/__init__.py @@ -1 +0,0 @@ -# diff --git a/tests/file_storage/__init__.py b/tests/file_storage/__init__.py index 8b13789179..e69de29bb2 100644 --- a/tests/file_storage/__init__.py +++ b/tests/file_storage/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/files/__init__.py b/tests/files/__init__.py index 8b13789179..e69de29bb2 100644 --- a/tests/files/__init__.py +++ b/tests/files/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py index 139597f9cb..e69de29bb2 100644 --- a/tests/fixtures/__init__.py +++ b/tests/fixtures/__init__.py @@ -1,2 +0,0 @@ - - diff --git a/tests/fixtures_model_package/__init__.py b/tests/fixtures_model_package/__init__.py index 139597f9cb..e69de29bb2 100644 --- a/tests/fixtures_model_package/__init__.py +++ b/tests/fixtures_model_package/__init__.py @@ -1,2 +0,0 @@ - - diff --git a/tests/forms_tests/tests/__init__.py b/tests/forms_tests/tests/__init__.py index 39219f82be..e69de29bb2 100644 --- a/tests/forms_tests/tests/__init__.py +++ b/tests/forms_tests/tests/__init__.py @@ -1,19 +0,0 @@ -from .test_error_messages import (FormsErrorMessagesTestCase, - ModelChoiceFieldErrorMessagesTestCase) -from .test_extra import FormsExtraTestCase, FormsExtraL10NTestCase -from .test_fields import FieldsTests -from .test_forms import FormsTestCase -from .test_formsets import (FormsFormsetTestCase, FormsetAsFooTests, - TestIsBoundBehavior, TestEmptyFormSet) -from .test_input_formats import (LocalizedTimeTests, CustomTimeInputFormatsTests, - SimpleTimeFormatTests, LocalizedDateTests, CustomDateInputFormatsTests, - SimpleDateFormatTests, LocalizedDateTimeTests, - CustomDateTimeInputFormatsTests, SimpleDateTimeFormatTests) -from .test_media import FormsMediaTestCase, StaticFormsMediaTestCase -from .tests import (TestTicket12510, TestTicket14567, ModelFormCallableModelDefault, - FormsModelTestCase, RelatedModelFormTests) -from .test_regressions import FormsRegressionsTestCase -from .test_util import FormsUtilTestCase -from .test_validators import TestFieldWithValidators -from .test_widgets import (FormsWidgetTestCase, FormsI18NWidgetsTestCase, - WidgetTests, LiveWidgetTests, ClearableFileInputTests) diff --git a/tests/inspectdb/__init__.py b/tests/inspectdb/__init__.py index 8b13789179..e69de29bb2 100644 --- a/tests/inspectdb/__init__.py +++ b/tests/inspectdb/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/invalid_models/__init__.py b/tests/invalid_models/__init__.py index 8b13789179..e69de29bb2 100644 --- a/tests/invalid_models/__init__.py +++ b/tests/invalid_models/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/m2m_signals/__init__.py b/tests/m2m_signals/__init__.py index 8b13789179..e69de29bb2 100644 --- a/tests/m2m_signals/__init__.py +++ b/tests/m2m_signals/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/m2m_through/__init__.py b/tests/m2m_through/__init__.py index 139597f9cb..e69de29bb2 100644 --- a/tests/m2m_through/__init__.py +++ b/tests/m2m_through/__init__.py @@ -1,2 +0,0 @@ - - diff --git a/tests/m2m_through_regress/__init__.py b/tests/m2m_through_regress/__init__.py index 139597f9cb..e69de29bb2 100644 --- a/tests/m2m_through_regress/__init__.py +++ b/tests/m2m_through_regress/__init__.py @@ -1,2 +0,0 @@ - - diff --git a/tests/mail/__init__.py b/tests/mail/__init__.py index 139597f9cb..e69de29bb2 100644 --- a/tests/mail/__init__.py +++ b/tests/mail/__init__.py @@ -1,2 +0,0 @@ - - diff --git a/tests/max_lengths/__init__.py b/tests/max_lengths/__init__.py index 8b13789179..e69de29bb2 100644 --- a/tests/max_lengths/__init__.py +++ b/tests/max_lengths/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/model_package/__init__.py b/tests/model_package/__init__.py index 8b13789179..e69de29bb2 100644 --- a/tests/model_package/__init__.py +++ b/tests/model_package/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/requests/__init__.py b/tests/requests/__init__.py index 3a328850c9..e69de29bb2 100644 --- a/tests/requests/__init__.py +++ b/tests/requests/__init__.py @@ -1,3 +0,0 @@ -""" -Tests for Django's various Request objects. -""" diff --git a/tests/select_for_update/__init__.py b/tests/select_for_update/__init__.py index 792d600548..e69de29bb2 100644 --- a/tests/select_for_update/__init__.py +++ b/tests/select_for_update/__init__.py @@ -1 +0,0 @@ -# diff --git a/tests/unmanaged_models/__init__.py b/tests/unmanaged_models/__init__.py index 139597f9cb..e69de29bb2 100644 --- a/tests/unmanaged_models/__init__.py +++ b/tests/unmanaged_models/__init__.py @@ -1,2 +0,0 @@ - - diff --git a/tests/view_tests/app0/__init__.py b/tests/view_tests/app0/__init__.py index 792d600548..e69de29bb2 100644 --- a/tests/view_tests/app0/__init__.py +++ b/tests/view_tests/app0/__init__.py @@ -1 +0,0 @@ -# diff --git a/tests/view_tests/app1/__init__.py b/tests/view_tests/app1/__init__.py index 792d600548..e69de29bb2 100644 --- a/tests/view_tests/app1/__init__.py +++ b/tests/view_tests/app1/__init__.py @@ -1 +0,0 @@ -# diff --git a/tests/view_tests/app2/__init__.py b/tests/view_tests/app2/__init__.py index 792d600548..e69de29bb2 100644 --- a/tests/view_tests/app2/__init__.py +++ b/tests/view_tests/app2/__init__.py @@ -1 +0,0 @@ -# diff --git a/tests/view_tests/app3/__init__.py b/tests/view_tests/app3/__init__.py index 792d600548..e69de29bb2 100644 --- a/tests/view_tests/app3/__init__.py +++ b/tests/view_tests/app3/__init__.py @@ -1 +0,0 @@ -# diff --git a/tests/view_tests/app4/__init__.py b/tests/view_tests/app4/__init__.py index 792d600548..e69de29bb2 100644 --- a/tests/view_tests/app4/__init__.py +++ b/tests/view_tests/app4/__init__.py @@ -1 +0,0 @@ -# diff --git a/tests/view_tests/tests/__init__.py b/tests/view_tests/tests/__init__.py index dae149a8ef..e69de29bb2 100644 --- a/tests/view_tests/tests/__init__.py +++ b/tests/view_tests/tests/__init__.py @@ -1,8 +0,0 @@ -from .test_debug import (DebugViewTests, ExceptionReporterTests, - ExceptionReporterTests, PlainTextReportTests, ExceptionReporterFilterTests, - AjaxResponseExceptionReporterFilter) -from .test_defaults import DefaultsTests -from .test_i18n import JsI18NTests, I18NTests, JsI18NTestsMultiPackage, JavascriptI18nTests -from .test_shortcuts import ShortcutTests -from .test_specials import URLHandling -from .test_static import StaticHelperTest, StaticUtilsTests, StaticTests From 522d3d61325811e7bec120ed1f3c2a5f89f701a0 Mon Sep 17 00:00:00 2001 From: "Romain B." <diskun13@gmail.com> Date: Mon, 9 Sep 2013 19:02:41 -0400 Subject: [PATCH 16/58] Fixed a little mistake in Django 1.7 release notes --- docs/releases/1.7.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index c49ad86c6c..e84f738530 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -47,7 +47,7 @@ but a few of the key features are: * A new ``makemigrations`` command provides an easy way to autodetect changes to your models and make migrations for them. -* :data:`~django.db.models.signals.post_syncdb` and +* :data:`~django.db.models.signals.pre_syncdb` and :data:`~django.db.models.signals.post_syncdb` have been renamed to :data:`~django.db.models.signals.pre_migrate` and :data:`~django.db.models.signals.post_migrate` respectively. The From 7008ed61c519f93a9b6c5c547ad718ad2deb959b Mon Sep 17 00:00:00 2001 From: homm <homm86@gmail.com> Date: Wed, 4 Sep 2013 20:08:13 +0400 Subject: [PATCH 17/58] Fixed #21033 -- Fixed uploaded filenames not always being truncated to 255 characters --- django/core/files/uploadedfile.py | 1 + tests/file_uploads/tests.py | 38 +++++++++++++++++++++---------- tests/files/tests.py | 1 - 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/django/core/files/uploadedfile.py b/django/core/files/uploadedfile.py index 9f948ca03b..0b632d39a7 100644 --- a/django/core/files/uploadedfile.py +++ b/django/core/files/uploadedfile.py @@ -46,6 +46,7 @@ class UploadedFile(File): # File names longer than 255 characters can cause problems on older OSes. if len(name) > 255: name, ext = os.path.splitext(name) + ext = ext[:255] name = name[:255 - len(ext)] + ext self._name = name diff --git a/tests/file_uploads/tests.py b/tests/file_uploads/tests.py index b0f00236d2..83a9e8b09c 100644 --- a/tests/file_uploads/tests.py +++ b/tests/file_uploads/tests.py @@ -167,16 +167,26 @@ class FileUploadTests(TestCase): def test_filename_overflow(self): """File names over 256 characters (dangerous on some platforms) get fixed up.""" - name = "%s.txt" % ("f"*500) - payload = client.FakePayload("\r\n".join([ - '--' + client.BOUNDARY, - 'Content-Disposition: form-data; name="file"; filename="%s"' % name, - 'Content-Type: application/octet-stream', - '', - 'Oops.' - '--' + client.BOUNDARY + '--', - '', - ])) + long_str = 'f' * 300 + cases = [ + # field name, filename, expected + ('long_filename', '%s.txt' % long_str, '%s.txt' % long_str[:251]), + ('long_extension', 'foo.%s' % long_str, '.%s' % long_str[:254]), + ('no_extension', long_str, long_str[:255]), + ('no_filename', '.%s' % long_str, '.%s' % long_str[:254]), + ('long_everything', '%s.%s' % (long_str, long_str), '.%s' % long_str[:254]), + ] + payload = client.FakePayload() + for name, filename, _ in cases: + payload.write("\r\n".join([ + '--' + client.BOUNDARY, + 'Content-Disposition: form-data; name="{0}"; filename="{1}"', + 'Content-Type: application/octet-stream', + '', + 'Oops.', + '' + ]).format(name, filename)) + payload.write('\r\n--' + client.BOUNDARY + '--\r\n') r = { 'CONTENT_LENGTH': len(payload), 'CONTENT_TYPE': client.MULTIPART_CONTENT, @@ -184,8 +194,12 @@ class FileUploadTests(TestCase): 'REQUEST_METHOD': 'POST', 'wsgi.input': payload, } - got = json.loads(self.client.request(**r).content.decode('utf-8')) - self.assertTrue(len(got['file']) < 256, "Got a long file name (%s characters)." % len(got['file'])) + result = json.loads(self.client.request(**r).content.decode('utf-8')) + for name, _, expected in cases: + got = result[name] + self.assertEqual(expected, got, 'Mismatch for {0}'.format(name)) + self.assertTrue(len(got) < 256, + "Got a long file name (%s characters)." % len(got)) def test_content_type_extra(self): """Uploaded files may have content type parameters available.""" diff --git a/tests/files/tests.py b/tests/files/tests.py index 2bc9d566d8..fbc1b1fe6c 100644 --- a/tests/files/tests.py +++ b/tests/files/tests.py @@ -132,7 +132,6 @@ class FileStorageTests(TestCase): self.assertEqual(f.read(), b'content') - class FileTests(unittest.TestCase): def test_context_manager(self): orig_file = tempfile.TemporaryFile() From f2a44528825ac07ca28c8bb7dc01b4375df8dc2c Mon Sep 17 00:00:00 2001 From: e0ne <e0ne@e0ne.info> Date: Mon, 9 Sep 2013 12:40:37 +0300 Subject: [PATCH 18/58] Fixed #18403 -- Initialized bad_cookies in SimpleCookie Thanks Stefano Crosta for the report. --- django/http/cookie.py | 2 ++ tests/httpwrappers/tests.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/django/http/cookie.py b/django/http/cookie.py index 40cf58def8..eef0c35759 100644 --- a/django/http/cookie.py +++ b/django/http/cookie.py @@ -64,6 +64,8 @@ else: M.set(key, real_value, coded_value) dict.__setitem__(self, key, M) except http_cookies.CookieError: + if not hasattr(self, 'bad_cookies'): + self.bad_cookies = set() self.bad_cookies.add(key) dict.__setitem__(self, key, http_cookies.Morsel()) diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py index 17bb98e24d..287d800c21 100644 --- a/tests/httpwrappers/tests.py +++ b/tests/httpwrappers/tests.py @@ -618,3 +618,12 @@ class CookieTests(unittest.TestCase): c = SimpleCookie() c.load({'name': 'val'}) self.assertEqual(c['name'].value, 'val') + + @unittest.skipUnless(six.PY2, "PY3 throws an exception on invalid cookie keys.") + def test_bad_cookie(self): + """ + Regression test for #18403 + """ + r = HttpResponse() + r.set_cookie("a:.b/", 1) + self.assertEqual(len(r.cookies.bad_cookies), 1) From 30fc49a7ca0d030c7855f31ed44395903fa6abdd Mon Sep 17 00:00:00 2001 From: John Hensley <john@cabincode.com> Date: Fri, 6 Sep 2013 15:01:54 -0400 Subject: [PATCH 19/58] Fixed #21057 -- Prevented FileSystemStorage from leaving temporary files. --- django/core/files/storage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django/core/files/storage.py b/django/core/files/storage.py index c1e654beeb..b9f3e01f0d 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -215,6 +215,7 @@ class FileSystemStorage(Storage): _file = os.fdopen(fd, mode) _file.write(chunk) finally: + content.close() locks.unlock(fd) if _file is not None: _file.close() From 2bc51438664b5ffbbd1430b4f9f3307f18b2b9db Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Tue, 10 Sep 2013 08:48:03 -0400 Subject: [PATCH 20/58] Fixed #7467 -- Added a template block to override the admin welcome message. Thanks Jeff Kowalczyk for the suggestion and rctay for the patch. --- django/contrib/admin/templates/admin/base.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index 9c79a4e698..d93128a140 100644 --- a/django/contrib/admin/templates/admin/base.html +++ b/django/contrib/admin/templates/admin/base.html @@ -26,8 +26,10 @@ </div> {% if user.is_active and user.is_staff %} <div id="user-tools"> - {% trans 'Welcome,' %} - <strong>{% firstof user.get_short_name user.get_username %}</strong>. + {% block welcome-msg %} + {% trans 'Welcome,' %} + <strong>{% firstof user.get_short_name user.get_username %}</strong>. + {% endblock %} {% block userlinks %} {% url 'django-admindocs-docroot' as docsroot %} {% if docsroot %} From af67ce5e18525ba52ee4533229468ed07f912536 Mon Sep 17 00:00:00 2001 From: Roberto Aguilar <roberto@baremetal.io> Date: Sat, 7 Sep 2013 21:12:13 +0000 Subject: [PATCH 21/58] Fixed #4574 -- Added CSS classes to the admin calendar widget for better control over styling. --- .../admin/js/admin/DateTimeShortcuts.js | 3 +- .../contrib/admin/static/admin/js/calendar.js | 43 +++++++++-- tests/admin_widgets/tests.py | 73 +++++++++++++++++++ 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js index aa12f8cf01..754c4f71a6 100644 --- a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js +++ b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js @@ -293,8 +293,9 @@ var DateTimeShortcuts = { var date_parts = inp.value.split('-'); var year = date_parts[0]; var month = parseFloat(date_parts[1]); + var selected = new Date(inp.value); if (year.match(/\d\d\d\d/) && month >= 1 && month <= 12) { - DateTimeShortcuts.calendars[num].drawDate(month, year); + DateTimeShortcuts.calendars[num].drawDate(month, year, selected); } } diff --git a/django/contrib/admin/static/admin/js/calendar.js b/django/contrib/admin/static/admin/js/calendar.js index 8e38c41c68..458eece92f 100644 --- a/django/contrib/admin/static/admin/js/calendar.js +++ b/django/contrib/admin/static/admin/js/calendar.js @@ -27,13 +27,29 @@ var CalendarNamespace = { } return days; }, - draw: function(month, year, div_id, callback) { // month = 1-12, year = 1-9999 + draw: function(month, year, div_id, callback, selected) { // month = 1-12, year = 1-9999 var today = new Date(); var todayDay = today.getDate(); var todayMonth = today.getMonth()+1; var todayYear = today.getFullYear(); var todayClass = ''; + // Use UTC functions here because the date field does not contain time + // and using the UTC function variants prevent the local time offset + // from altering the date, specifically the day field. For example: + // + // ``` + // var x = new Date('2013-10-02'); + // var day = x.getDate(); + // ``` + // + // The day variable above will be 1 instead of 2 in, say, US Pacific time + // zone. + var isSelectedMonth = false; + if (typeof selected != 'undefined') { + isSelectedMonth = (selected.getUTCFullYear() == year && (selected.getUTCMonth()+1) == month); + } + month = parseInt(month); year = parseInt(year); var calDiv = document.getElementById(div_id); @@ -55,7 +71,7 @@ var CalendarNamespace = { tableRow = quickElement('tr', tableBody); for (var i = 0; i < startingPos; i++) { var _cell = quickElement('td', tableRow, ' '); - _cell.style.backgroundColor = '#f3f3f3'; + _cell.className = "nonday"; } // Draw days of month @@ -69,6 +85,13 @@ var CalendarNamespace = { } else { todayClass=''; } + + // use UTC function; see above for explanation. + if (isSelectedMonth && currentDay == selected.getUTCDate()) { + if (todayClass != '') todayClass += " "; + todayClass += "selected"; + } + var cell = quickElement('td', tableRow, '', 'class', todayClass); quickElement('a', cell, currentDay, 'href', 'javascript:void(' + callback + '('+year+','+month+','+currentDay+'));'); @@ -78,7 +101,7 @@ var CalendarNamespace = { // Draw blanks after end of month (optional, but makes for valid code) while (tableRow.childNodes.length < 7) { var _cell = quickElement('td', tableRow, ' '); - _cell.style.backgroundColor = '#f3f3f3'; + _cell.className = "nonday"; } calDiv.appendChild(calTable); @@ -86,7 +109,7 @@ var CalendarNamespace = { } // Calendar -- A calendar instance -function Calendar(div_id, callback) { +function Calendar(div_id, callback, selected) { // div_id (string) is the ID of the element in which the calendar will // be displayed // callback (string) is the name of a JavaScript function that will be @@ -97,14 +120,22 @@ function Calendar(div_id, callback) { this.today = new Date(); this.currentMonth = this.today.getMonth() + 1; this.currentYear = this.today.getFullYear(); + if (typeof selected != 'undefined') { + this.selected = selected; + } } Calendar.prototype = { drawCurrent: function() { - CalendarNamespace.draw(this.currentMonth, this.currentYear, this.div_id, this.callback); + CalendarNamespace.draw(this.currentMonth, this.currentYear, this.div_id, this.callback, this.selected); }, - drawDate: function(month, year) { + drawDate: function(month, year, selected) { this.currentMonth = month; this.currentYear = year; + + if(selected) { + this.selected = selected; + } + this.drawCurrent(); }, drawPreviousMonth: function() { diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index 95449ff47c..7eb68be4d0 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -549,6 +549,79 @@ class DateTimePickerSeleniumFirefoxTests(AdminSeleniumWebDriverTestCase): self.assertEqual( self.get_css_value('#clockbox0', 'display'), 'none') + def test_calendar_nonday_class(self): + """ + Ensure cells that are not days of the month have the `nonday` CSS class. + Refs #4574. + """ + self.admin_login(username='super', password='secret', login_url='/') + # Open a page that has a date and time picker widgets + self.selenium.get('%s%s' % (self.live_server_url, + '/admin_widgets/member/add/')) + + # fill in the birth date. + self.selenium.find_element_by_id('id_birthdate_0').send_keys('2013-06-01') + + # Click the calendar icon + self.selenium.find_element_by_id('calendarlink0').click() + + # get all the tds within the calendar + calendar0 = self.selenium.find_element_by_id('calendarin0') + tds = calendar0.find_elements_by_tag_name('td') + + # make sure the first and last 6 cells have class nonday + for td in tds[:6] + tds[-6:]: + self.assertEqual(td.get_attribute('class'), 'nonday') + + def test_calendar_selected_class(self): + """ + Ensure cell for the day in the input has the `selected` CSS class. + Refs #4574. + """ + self.admin_login(username='super', password='secret', login_url='/') + # Open a page that has a date and time picker widgets + self.selenium.get('%s%s' % (self.live_server_url, + '/admin_widgets/member/add/')) + + # fill in the birth date. + self.selenium.find_element_by_id('id_birthdate_0').send_keys('2013-06-01') + + # Click the calendar icon + self.selenium.find_element_by_id('calendarlink0').click() + + # get all the tds within the calendar + calendar0 = self.selenium.find_element_by_id('calendarin0') + tds = calendar0.find_elements_by_tag_name('td') + + # verify the selected cell + selected = tds[6] + self.assertEqual(selected.get_attribute('class'), 'selected') + + self.assertEqual(selected.text, '1') + + def test_calendar_no_selected_class(self): + """ + Ensure no cells are given the selected class when the field is empty. + Refs #4574. + """ + self.admin_login(username='super', password='secret', login_url='/') + # Open a page that has a date and time picker widgets + self.selenium.get('%s%s' % (self.live_server_url, + '/admin_widgets/member/add/')) + + # Click the calendar icon + self.selenium.find_element_by_id('calendarlink0').click() + + # get all the tds within the calendar + calendar0 = self.selenium.find_element_by_id('calendarin0') + tds = calendar0.find_elements_by_tag_name('td') + + # verify there are no cells with the selected class + selected = [td for td in tds if td.get_attribute('class') == 'selected'] + + self.assertEqual(len(selected), 0) + + class DateTimePickerSeleniumChromeTests(DateTimePickerSeleniumFirefoxTests): webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver' From fca4c4826e7b4cec84c3f8140bb929e38eea962c Mon Sep 17 00:00:00 2001 From: oz123 <nahumoz@gmail.com> Date: Tue, 10 Sep 2013 14:39:51 +0200 Subject: [PATCH 22/58] Fixed #21075 - Improved doc for calling call_command with arguments. --- docs/ref/django-admin.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 93fc4de4ab..5bc9a2b83e 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1638,6 +1638,15 @@ Examples:: management.call_command('flush', verbosity=0, interactive=False) management.call_command('loaddata', 'test_data', verbosity=0) +Note that command options that take no arguments are passed as keywords +with ``True`` or ``False``:: + + management.call_command('dumpdata', use_natural_keys=True) + +Command options which take multiple options are passed a list:: + + management.call_command('dumpdata', exclude=['contenttypes', 'auth']) + Output redirection ================== From 4ba373840a7b299fff4a7bcc1001e32dffceee86 Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Tue, 10 Sep 2013 09:49:39 -0400 Subject: [PATCH 23/58] Fixed #16534 -- Improved ability to customize DiscoverRunner Added DiscoverRunner.test_suite and .test_runner attributes. Thanks tomchristie for the suggestion and jcd for the patch. --- django/test/runner.py | 12 ++++++++---- docs/releases/1.7.txt | 8 ++++++++ docs/topics/testing/advanced.txt | 19 +++++++++++++++++++ tests/test_runner/test_discover_runner.py | 11 ++++++++++- 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/django/test/runner.py b/django/test/runner.py index 84fe2499f1..700a52c775 100644 --- a/django/test/runner.py +++ b/django/test/runner.py @@ -14,6 +14,8 @@ class DiscoverRunner(object): A Django test runner that uses unittest2 test discovery. """ + test_suite = TestSuite + test_runner = unittest.TextTestRunner test_loader = defaultTestLoader reorder_by = (TestCase, ) option_list = ( @@ -42,7 +44,7 @@ class DiscoverRunner(object): unittest.installHandler() def build_suite(self, test_labels=None, extra_tests=None, **kwargs): - suite = TestSuite() + suite = self.test_suite() test_labels = test_labels or ['.'] extra_tests = extra_tests or [] @@ -107,7 +109,7 @@ class DiscoverRunner(object): return setup_databases(self.verbosity, self.interactive, **kwargs) def run_suite(self, suite, **kwargs): - return unittest.TextTestRunner( + return self.test_runner( verbosity=self.verbosity, failfast=self.failfast, ).run(suite) @@ -201,7 +203,8 @@ def reorder_suite(suite, classes): classes[1], etc. Tests with no match in classes are placed last. """ class_count = len(classes) - bins = [unittest.TestSuite() for i in range(class_count+1)] + suite_class = type(suite) + bins = [suite_class() for i in range(class_count+1)] partition_suite(suite, classes, bins) for i in range(class_count): bins[0].addTests(bins[i+1]) @@ -218,8 +221,9 @@ def partition_suite(suite, classes, bins): Tests of type classes[i] are added to bins[i], tests with no match found in classes are place in bins[-1] """ + suite_class = type(suite) for test in suite: - if isinstance(test, unittest.TestSuite): + if isinstance(test, suite_class): partition_suite(test, classes, bins) else: for i in range(len(classes)): diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index e84f738530..9f05055fd2 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -285,6 +285,14 @@ Templates * ``TypeError`` exceptions are not longer silenced when raised during the rendering of a template. +Tests +^^^^^ + +* :class:`~django.test.runner.DiscoverRunner` has two new attributes, + :attr:`~django.test.runner.DiscoverRunner.test_suite` and + :attr:`~django.test.runner.DiscoverRunner.test_runner`, which facilitate + overriding the way tests are collected and run. + Backwards incompatible changes in 1.7 ===================================== diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index a4c425aeb9..d02912dd31 100644 --- a/docs/topics/testing/advanced.txt +++ b/docs/topics/testing/advanced.txt @@ -338,6 +338,25 @@ execute and tear down the test suite. Attributes ~~~~~~~~~~ +.. attribute:: DiscoverRunner.test_suite + + .. versionadded:: 1.7 + + The class used to build the test suite. By default it is set to + ``unittest.TestSuite``. This can be overridden if you wish to implement + different logic for collecting tests. + +.. attribute:: DiscoverRunner.test_runner + + .. versionadded:: 1.7 + + This is the class of the low-level test runner which is used to execute + the individual tests and format the results. By default it is set to + ``unittest.TextTestRunner``. Despite the unfortunate similarity in + naming conventions, this is not the same type of class as + ``DiscoverRunner``, which covers a broader set of responsibilites. You + can override this attribute to modify the way tests are run and reported. + .. attribute:: DiscoverRunner.test_loader This is the class that loads tests, whether from TestCases or modules or diff --git a/tests/test_runner/test_discover_runner.py b/tests/test_runner/test_discover_runner.py index 4494b2bd3b..bd26e2e73d 100644 --- a/tests/test_runner/test_discover_runner.py +++ b/tests/test_runner/test_discover_runner.py @@ -1,7 +1,7 @@ from contextlib import contextmanager import os import sys -from unittest import expectedFailure +from unittest import expectedFailure, TestSuite, TextTestRunner, defaultTestLoader from django.test import TestCase from django.test.runner import DiscoverRunner @@ -68,3 +68,12 @@ class DiscoverRunnerTest(TestCase): ).countTestCases() self.assertEqual(count, 3) + + def test_overrideable_test_suite(self): + self.assertEqual(DiscoverRunner().test_suite, TestSuite) + + def test_overrideable_test_runner(self): + self.assertEqual(DiscoverRunner().test_runner, TextTestRunner) + + def test_overrideable_test_loader(self): + self.assertEqual(DiscoverRunner().test_loader, defaultTestLoader) From 0ac7cc32655c670168b3c111cef1101ed1a09d1a Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Tue, 10 Sep 2013 09:54:26 -0400 Subject: [PATCH 24/58] Fixed #21083 - Fixed spelling in tutorial. Thanks jimmy.kjaersgaard at gmail.com for the report. --- docs/intro/tutorial05.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/intro/tutorial05.txt b/docs/intro/tutorial05.txt index 14a1795a1c..acad576f70 100644 --- a/docs/intro/tutorial05.txt +++ b/docs/intro/tutorial05.txt @@ -494,7 +494,7 @@ class:: """ The questions index page may display multiple questions. """ - create_question(question_text="Past quesiton 1.", days=-30) + create_question(question_text="Past question 1.", days=-30) create_question(question_text="Past question 2.", days=-5) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( From 5df8f749e6338c85a091f41803e6ecb280fc9f70 Mon Sep 17 00:00:00 2001 From: Loic Bistuer <loic.bistuer@sixmedia.com> Date: Mon, 2 Sep 2013 01:21:08 +0700 Subject: [PATCH 25/58] Fixed #20978 -- Made deletion.SET_NULL more friendly for MigrationWriter.serialize. --- django/db/models/deletion.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index 769e5b9bb6..6f89da7417 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -38,7 +38,8 @@ def SET(value): return set_on_delete -SET_NULL = SET(None) +def SET_NULL(collector, field, sub_objs, using): + collector.add_field_update(field, None, sub_objs) def SET_DEFAULT(collector, field, sub_objs, using): From d59f1993f150f83524051d96b52df08da4dcf011 Mon Sep 17 00:00:00 2001 From: Loic Bistuer <loic.bistuer@sixmedia.com> Date: Mon, 2 Sep 2013 13:02:07 +0700 Subject: [PATCH 26/58] Made MigrationWriter look for a "deconstruct" attribute on functions. Refs #20978. --- django/db/migrations/writer.py | 40 +++++++++++++++++++-------------- django/db/models/deletion.py | 1 + tests/migrations/test_writer.py | 4 ++++ 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/django/db/migrations/writer.py b/django/db/migrations/writer.py index fa4afbeabf..756bb97c04 100644 --- a/django/db/migrations/writer.py +++ b/django/db/migrations/writer.py @@ -73,6 +73,26 @@ class MigrationWriter(object): raise ImportError("Cannot open migrations module %s for app %s" % (migrations_module_name, self.migration.app_label)) return os.path.join(basedir, self.filename) + @classmethod + def serialize_deconstructed(cls, path, args, kwargs): + module, name = path.rsplit(".", 1) + if module == "django.db.models": + imports = set(["from django.db import models"]) + name = "models.%s" % name + else: + imports = set(["import %s" % module]) + name = path + arg_strings = [] + for arg in args: + arg_string, arg_imports = cls.serialize(arg) + arg_strings.append(arg_string) + imports.update(arg_imports) + for kw, arg in kwargs.items(): + arg_string, arg_imports = cls.serialize(arg) + imports.update(arg_imports) + arg_strings.append("%s=%s" % (kw, arg_string)) + return "%s(%s)" % (name, ", ".join(arg_strings)), imports + @classmethod def serialize(cls, value): """ @@ -119,23 +139,7 @@ class MigrationWriter(object): # Django fields elif isinstance(value, models.Field): attr_name, path, args, kwargs = value.deconstruct() - module, name = path.rsplit(".", 1) - if module == "django.db.models": - imports = set(["from django.db import models"]) - name = "models.%s" % name - else: - imports = set(["import %s" % module]) - name = path - arg_strings = [] - for arg in args: - arg_string, arg_imports = cls.serialize(arg) - arg_strings.append(arg_string) - imports.update(arg_imports) - for kw, arg in kwargs.items(): - arg_string, arg_imports = cls.serialize(arg) - imports.update(arg_imports) - arg_strings.append("%s=%s" % (kw, arg_string)) - return "%s(%s)" % (name, ", ".join(arg_strings)), imports + return cls.serialize_deconstructed(path, args, kwargs) # Functions elif isinstance(value, (types.FunctionType, types.BuiltinFunctionType)): # Special-cases, as these don't have im_class @@ -152,6 +156,8 @@ class MigrationWriter(object): klass = value.im_class module = klass.__module__ return "%s.%s.%s" % (module, klass.__name__, value.__name__), set(["import %s" % module]) + elif hasattr(value, 'deconstruct'): + return cls.serialize_deconstructed(*value.deconstruct()) elif value.__name__ == '<lambda>': raise ValueError("Cannot serialize function: lambda") elif value.__module__ is None: diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index 6f89da7417..90e515cce5 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -35,6 +35,7 @@ def SET(value): else: def set_on_delete(collector, field, sub_objs, using): collector.add_field_update(field, value, sub_objs) + set_on_delete.deconstruct = lambda: ('django.db.models.SET', (value,), {}) return set_on_delete diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 022628803b..bf9f55aff0 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -63,6 +63,10 @@ class WriterTests(TestCase): # Functions with six.assertRaisesRegex(self, ValueError, 'Cannot serialize function: lambda'): self.assertSerializedEqual(lambda x: 42) + self.assertSerializedEqual(models.SET_NULL) + string, imports = MigrationWriter.serialize(models.SET(42)) + self.assertEqual(string, 'models.SET(42)') + self.serialize_round_trip(models.SET(42)) # Datetime stuff self.assertSerializedEqual(datetime.datetime.utcnow()) self.assertSerializedEqual(datetime.datetime.utcnow) From b2b763448f726ee952743596e9a34fcb154bdb12 Mon Sep 17 00:00:00 2001 From: Gregor MacGregor <Timothy.J.Clifford@gmail.com> Date: Fri, 6 Sep 2013 13:24:52 -0500 Subject: [PATCH 27/58] Fixed #20841 -- Added messages to NotImplementedErrors Thanks joseph at vertstudios.com for the suggestion. --- django/contrib/admin/filters.py | 12 ++--- django/contrib/auth/hashers.py | 6 +-- django/contrib/auth/models.py | 12 ++--- .../contrib/comments/templatetags/comments.py | 3 +- django/contrib/gis/db/backends/base.py | 10 ++--- django/contrib/messages/storage/base.py | 4 +- django/contrib/sessions/backends/base.py | 12 ++--- django/contrib/staticfiles/finders.py | 4 +- django/core/cache/backends/base.py | 10 ++--- django/core/files/storage.py | 16 +++---- django/core/files/uploadhandler.py | 4 +- django/core/mail/backends/base.py | 2 +- django/core/management/base.py | 8 ++-- django/core/serializers/base.py | 12 ++--- django/db/backends/__init__.py | 44 +++++++++---------- django/db/backends/schema.py | 2 +- django/db/migrations/operations/base.py | 6 +-- django/forms/widgets.py | 2 +- django/template/base.py | 4 +- django/template/loader.py | 2 +- django/utils/archive.py | 4 +- django/utils/dateformat.py | 2 +- django/utils/feedgenerator.py | 2 +- django/utils/functional.py | 2 +- django/utils/regex_helper.py | 2 +- tests/mail/tests.py | 4 +- 26 files changed, 96 insertions(+), 95 deletions(-) diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py index 2a9fb2a8f9..7bba3a797f 100644 --- a/django/contrib/admin/filters.py +++ b/django/contrib/admin/filters.py @@ -33,26 +33,26 @@ class ListFilter(object): """ Returns True if some choices would be output for this filter. """ - raise NotImplementedError + raise NotImplementedError('subclasses of ListFilter must provide a has_output() method') def choices(self, cl): """ Returns choices ready to be output in the template. """ - raise NotImplementedError + raise NotImplementedError('subclasses of ListFilter must provide a choices() method') def queryset(self, request, queryset): """ Returns the filtered queryset. """ - raise NotImplementedError + raise NotImplementedError('subclasses of ListFilter must provide a queryset() method') def expected_parameters(self): """ Returns the list of parameter names that are expected from the request's query string and that will be used by this filter. """ - raise NotImplementedError + raise NotImplementedError('subclasses of ListFilter must provide an expected_parameters() method') class SimpleListFilter(ListFilter): @@ -89,7 +89,9 @@ class SimpleListFilter(ListFilter): """ Must be overridden to return a list of tuples (value, verbose value) """ - raise NotImplementedError + raise NotImplementedError( + 'The SimpleListFilter.lookups() method must be overridden to ' + 'return a list of tuples (value, verbose value)') def expected_parameters(self): return [self.parameter_name] diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index 41e3deff67..3b598959bc 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -192,7 +192,7 @@ class BasePasswordHasher(object): """ Checks if the given password is correct """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BasePasswordHasher must provide a verify() method') def encode(self, password, salt): """ @@ -201,7 +201,7 @@ class BasePasswordHasher(object): The result is normally formatted as "algorithm$salt$hash" and must be fewer than 128 characters. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BasePasswordHasher must provide an encode() method') def safe_summary(self, encoded): """ @@ -210,7 +210,7 @@ class BasePasswordHasher(object): The result is a dictionary and will be used where the password field must be displayed to construct a safe representation of the password. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BasePasswordHasher must provide a safe_summary() method') class PBKDF2PasswordHasher(BasePasswordHasher): diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 4c88e7d1ba..8db304f620 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -245,10 +245,10 @@ class AbstractBaseUser(models.Model): return is_password_usable(self.password) def get_full_name(self): - raise NotImplementedError() + raise NotImplementedError('subclasses of AbstractBaseUser must provide a get_full_name() method') def get_short_name(self): - raise NotImplementedError() + raise NotImplementedError('subclasses of AbstractBaseUser must provide a get_short_name() method.') # A few helper functions for common logic between User and AnonymousUser. @@ -441,16 +441,16 @@ class AnonymousUser(object): return 1 # instances always return the same hash value def save(self): - raise NotImplementedError + raise NotImplementedError("Django doesn't provide a DB representation for AnonymousUser.") def delete(self): - raise NotImplementedError + raise NotImplementedError("Django doesn't provide a DB representation for AnonymousUser.") def set_password(self, raw_password): - raise NotImplementedError + raise NotImplementedError("Django doesn't provide a DB representation for AnonymousUser.") def check_password(self, raw_password): - raise NotImplementedError + raise NotImplementedError("Django doesn't provide a DB representation for AnonymousUser.") def _get_groups(self): return self._groups diff --git a/django/contrib/comments/templatetags/comments.py b/django/contrib/comments/templatetags/comments.py index 2b2cea5f20..44ac3d588b 100644 --- a/django/contrib/comments/templatetags/comments.py +++ b/django/contrib/comments/templatetags/comments.py @@ -112,7 +112,7 @@ class BaseCommentNode(six.with_metaclass(RenameBaseCommentNodeMethods, template. def get_context_value_from_queryset(self, context, qs): """Subclasses should override this.""" - raise NotImplementedError + raise NotImplementedError('subclasses of BaseCommentNode must provide a get_context_value_from_queryset() method') class CommentListNode(BaseCommentNode): """Insert a list of comments into the context.""" @@ -338,4 +338,3 @@ def get_comment_permalink(comment, anchor_pattern=None): if anchor_pattern: return comment.get_absolute_url(anchor_pattern) return comment.get_absolute_url() - diff --git a/django/contrib/gis/db/backends/base.py b/django/contrib/gis/db/backends/base.py index 7db7ce51ba..ae2c920923 100644 --- a/django/contrib/gis/db/backends/base.py +++ b/django/contrib/gis/db/backends/base.py @@ -101,7 +101,7 @@ class BaseSpatialOperations(object): Returns the database column type for the geometry field on the spatial backend. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseSpatialOperations must provide a geo_db_type() method') def get_distance(self, f, value, lookup_type): """ @@ -117,7 +117,7 @@ class BaseSpatialOperations(object): stored procedure call to the transformation function of the spatial backend. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseSpatialOperations must provide a geo_db_placeholder() method') def get_expression_column(self, evaluator): """ @@ -134,14 +134,14 @@ class BaseSpatialOperations(object): raise NotImplementedError('Aggregate support not implemented for this spatial backend.') def spatial_lookup_sql(self, lvalue, lookup_type, value, field): - raise NotImplementedError + raise NotImplementedError('subclasses of BaseSpatialOperations must a provide spatial_lookup_sql() method') # Routines for getting the OGC-compliant models. def geometry_columns(self): - raise NotImplementedError + raise NotImplementedError('subclasses of BaseSpatialOperations must a provide geometry_columns() method') def spatial_ref_sys(self): - raise NotImplementedError + raise NotImplementedError('subclasses of BaseSpatialOperations must a provide spatial_ref_sys() method') @python_2_unicode_compatible class SpatialRefSysMixin(object): diff --git a/django/contrib/messages/storage/base.py b/django/contrib/messages/storage/base.py index 7fe8a077ed..c021fbd2ae 100644 --- a/django/contrib/messages/storage/base.py +++ b/django/contrib/messages/storage/base.py @@ -105,7 +105,7 @@ class BaseStorage(object): just containing no messages) then ``None`` should be returned in place of ``messages``. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseStorage must provide a _get() method') def _store(self, messages, response, *args, **kwargs): """ @@ -116,7 +116,7 @@ class BaseStorage(object): **This method must be implemented by a subclass.** """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseStorage must provide a _store() method') def _prepare_messages(self, messages): """ diff --git a/django/contrib/sessions/backends/base.py b/django/contrib/sessions/backends/base.py index 7f5e958a60..08e79530f4 100644 --- a/django/contrib/sessions/backends/base.py +++ b/django/contrib/sessions/backends/base.py @@ -284,7 +284,7 @@ class SessionBase(object): """ Returns True if the given session_key already exists. """ - raise NotImplementedError + raise NotImplementedError('subclasses of SessionBase must provide an exists() method') def create(self): """ @@ -292,7 +292,7 @@ class SessionBase(object): a unique key and will have saved the result once (with empty data) before the method returns. """ - raise NotImplementedError + raise NotImplementedError('subclasses of SessionBase must provide a create() method') def save(self, must_create=False): """ @@ -300,20 +300,20 @@ class SessionBase(object): is created (otherwise a CreateError exception is raised). Otherwise, save() can update an existing object with the same key. """ - raise NotImplementedError + raise NotImplementedError('subclasses of SessionBase must provide a save() method') def delete(self, session_key=None): """ Deletes the session data under this key. If the key is None, the current session key value is used. """ - raise NotImplementedError + raise NotImplementedError('subclasses of SessionBase must provide a delete() method') def load(self): """ Loads the session data and returns a dictionary. """ - raise NotImplementedError + raise NotImplementedError('subclasses of SessionBase must provide a load() method') @classmethod def clear_expired(cls): @@ -324,4 +324,4 @@ class SessionBase(object): NotImplementedError. If it isn't necessary, because the backend has a built-in expiration mechanism, it should be a no-op. """ - raise NotImplementedError + raise NotImplementedError('This backend does not support clear_expired().') diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py index d4efd1a8d8..3d93d2a447 100644 --- a/django/contrib/staticfiles/finders.py +++ b/django/contrib/staticfiles/finders.py @@ -28,7 +28,7 @@ class BaseFinder(object): the first found file path will be returned; if set to ``True`` a list of all found files paths is returned. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseFinder must provide a find() method') def list(self, ignore_patterns): """ @@ -36,7 +36,7 @@ class BaseFinder(object): a two item iterable consisting of the relative path and storage instance. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseFinder must provide a list() method') class FileSystemFinder(BaseFinder): diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py index deb98e7714..db76192354 100644 --- a/django/core/cache/backends/base.py +++ b/django/core/cache/backends/base.py @@ -96,27 +96,27 @@ class BaseCache(object): Returns True if the value was stored, False otherwise. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseCache must provide an add() method') def get(self, key, default=None, version=None): """ Fetch a given key from the cache. If the key does not exist, return default, which itself defaults to None. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseCache must provide a get() method') def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): """ Set a value in the cache. If timeout is given, that timeout will be used for the key; otherwise the default cache timeout will be used. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseCache must provide a set() method') def delete(self, key, version=None): """ Delete a key from the cache, failing silently. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseCache must provide a delete() method') def get_many(self, keys, version=None): """ @@ -190,7 +190,7 @@ class BaseCache(object): def clear(self): """Remove *all* values from the cache at once.""" - raise NotImplementedError + raise NotImplementedError('subclasses of BaseCache must provide a clear() method') def validate_key(self, key): """ diff --git a/django/core/files/storage.py b/django/core/files/storage.py index b9f3e01f0d..b42981cb5e 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -92,55 +92,55 @@ class Storage(object): """ Deletes the specified file from the storage system. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of Storage must provide a delete() method') def exists(self, name): """ Returns True if a file referened by the given name already exists in the storage system, or False if the name is available for a new file. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of Storage must provide a exists() method') def listdir(self, path): """ Lists the contents of the specified path, returning a 2-tuple of lists; the first item being directories, the second item being files. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of Storage must provide a listdir() method') def size(self, name): """ Returns the total size, in bytes, of the file specified by name. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of Storage must provide a size() method') def url(self, name): """ Returns an absolute URL where the file's contents can be accessed directly by a Web browser. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of Storage must provide a url() method') def accessed_time(self, name): """ Returns the last accessed time (as datetime object) of the file specified by name. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of Storage must provide an accessed_time() method') def created_time(self, name): """ Returns the creation time (as datetime object) of the file specified by name. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of Storage must provide a created_time() method') def modified_time(self, name): """ Returns the last modified time (as datetime object) of the file specified by name. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of Storage must provide a modified_time() method') class FileSystemStorage(Storage): """ diff --git a/django/core/files/uploadhandler.py b/django/core/files/uploadhandler.py index 6739b26e0c..914e2c51fa 100644 --- a/django/core/files/uploadhandler.py +++ b/django/core/files/uploadhandler.py @@ -104,7 +104,7 @@ class FileUploadHandler(object): Receive data from the streamed upload parser. ``start`` is the position in the file of the chunk. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of FileUploadHandler must provide a recieve_data_chunk() method') def file_complete(self, file_size): """ @@ -113,7 +113,7 @@ class FileUploadHandler(object): Subclasses should return a valid ``UploadedFile`` object. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of FileUploadHandler must provide a file_complete() method') def upload_complete(self): """ diff --git a/django/core/mail/backends/base.py b/django/core/mail/backends/base.py index 9a3092849d..2be4510363 100644 --- a/django/core/mail/backends/base.py +++ b/django/core/mail/backends/base.py @@ -36,4 +36,4 @@ class BaseEmailBackend(object): Sends one or more EmailMessage objects and returns the number of email messages sent. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseEmailBackend must override send_messages() method') diff --git a/django/core/management/base.py b/django/core/management/base.py index 9c8940e99f..1aba53dd01 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -325,7 +325,7 @@ class BaseCommand(object): this method. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseCommand must provide a handle() method') class AppCommand(BaseCommand): @@ -361,7 +361,7 @@ class AppCommand(BaseCommand): the command line. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of AppCommand must provide a handle_app() method') class LabelCommand(BaseCommand): @@ -397,7 +397,7 @@ class LabelCommand(BaseCommand): string as given on the command line. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of LabelCommand must provide a handle_label() method') class NoArgsCommand(BaseCommand): @@ -423,4 +423,4 @@ class NoArgsCommand(BaseCommand): Perform this command's actions. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of NoArgsCommand must provide a handle_noargs() method') diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index cd4f7ffb2b..da93c3154f 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -65,7 +65,7 @@ class Serializer(object): """ Called when serializing of the queryset starts. """ - raise NotImplementedError + raise NotImplementedError('subclasses of Serializer must provide a start_serialization() method') def end_serialization(self): """ @@ -77,7 +77,7 @@ class Serializer(object): """ Called when serializing of an object starts. """ - raise NotImplementedError + raise NotImplementedError('subclasses of Serializer must provide a start_object() method') def end_object(self, obj): """ @@ -89,19 +89,19 @@ class Serializer(object): """ Called to handle each individual (non-relational) field on an object. """ - raise NotImplementedError + raise NotImplementedError('subclasses of Serializer must provide an handle_field() method') def handle_fk_field(self, obj, field): """ Called to handle a ForeignKey field. """ - raise NotImplementedError + raise NotImplementedError('subclasses of Serializer must provide an handle_fk_field() method') def handle_m2m_field(self, obj, field): """ Called to handle a ManyToManyField. """ - raise NotImplementedError + raise NotImplementedError('subclasses of Serializer must provide an handle_m2m_field() method') def getvalue(self): """ @@ -135,7 +135,7 @@ class Deserializer(six.Iterator): def __next__(self): """Iteration iterface -- return the next item in the stream""" - raise NotImplementedError + raise NotImplementedError('subclasses of Deserializer must provide a __next__() method') class DeserializedObject(object): """ diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 6b1489d128..d22fea5ec2 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -84,19 +84,19 @@ class BaseDatabaseWrapper(object): def get_connection_params(self): """Returns a dict of parameters suitable for get_new_connection.""" - raise NotImplementedError + raise NotImplementedError('subclasses of BaseDatabaseWrapper may require a get_connection_params() method') def get_new_connection(self, conn_params): """Opens a connection to the database.""" - raise NotImplementedError + raise NotImplementedError('subclasses of BaseDatabaseWrapper may require a get_new_connection() method') def init_connection_state(self): """Initializes the database connection settings.""" - raise NotImplementedError + raise NotImplementedError('subclasses of BaseDatabaseWrapper may require an init_connection_state() method') def create_cursor(self): """Creates a cursor. Assumes that a connection is established.""" - raise NotImplementedError + raise NotImplementedError('subclasses of BaseDatabaseWrapper may require a create_cursor() method') ##### Backend-specific methods for creating connections ##### @@ -262,7 +262,7 @@ class BaseDatabaseWrapper(object): """ Backend-specific implementation to enable or disable autocommit. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseDatabaseWrapper may require a _set_autocommit() method') ##### Generic transaction management methods ##### @@ -440,7 +440,7 @@ class BaseDatabaseWrapper(object): Tests if the database connection is usable. This function may assume that self.connection is not None. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseDatabaseWrapper may require an is_usable() method') def close_if_unusable_or_obsolete(self): """ @@ -519,11 +519,11 @@ class BaseDatabaseWrapper(object): """ Only required when autocommits_when_autocommit_is_off = True. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseDatabaseWrapper may require a _start_transaction_under_autocommit() method') def schema_editor(self, *args, **kwargs): "Returns a new instance of this backend's SchemaEditor" - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseDatabaseWrapper may require a schema_editor() method') class BaseDatabaseFeatures(object): @@ -741,13 +741,13 @@ class BaseDatabaseOperations(object): Given a lookup_type of 'year', 'month' or 'day', returns the SQL that extracts a value from the given date field field_name. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a date_extract_sql() method') def date_interval_sql(self, sql, connector, timedelta): """ Implements the date interval functionality for expressions """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a date_interval_sql() method') def date_trunc_sql(self, lookup_type, field_name): """ @@ -755,7 +755,7 @@ class BaseDatabaseOperations(object): truncates the given date field field_name to a date object with only the given specificity. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a datetrunc_sql() method') def datetime_cast_sql(self): """ @@ -772,7 +772,7 @@ class BaseDatabaseOperations(object): 'second', returns the SQL that extracts a value from the given datetime field field_name, and a tuple of parameters. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a datetime_extract_sql() method') def datetime_trunc_sql(self, lookup_type, field_name, tzname): """ @@ -781,7 +781,7 @@ class BaseDatabaseOperations(object): field_name to a datetime object with only the given specificity, and a tuple of parameters. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a datetime_trunk_sql() method') def deferrable_sql(self): """ @@ -916,7 +916,7 @@ class BaseDatabaseOperations(object): Returns the value to use for the LIMIT when we are wanting "LIMIT infinity". Returns None if the limit clause can be omitted in this case. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a no_limit_value() method') def pk_default_value(self): """ @@ -956,7 +956,7 @@ class BaseDatabaseOperations(object): Returns a quoted version of the given table, index or column name. Does not quote the given name if it's already been quoted. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a quote_name() method') def quote_parameter(self, value): """ @@ -982,7 +982,7 @@ class BaseDatabaseOperations(object): If the feature is not supported (or part of it is not supported), a NotImplementedError exception can be raised. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a regex_lookup() method') def savepoint_create_sql(self, sid): """ @@ -1028,7 +1028,7 @@ class BaseDatabaseOperations(object): to tables with foreign keys pointing the tables being truncated. PostgreSQL requires a cascade even if these tables are empty. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseDatabaseOperations must provide a sql_flush() method') def sequence_reset_by_name_sql(self, style, sequences): """ @@ -1245,7 +1245,7 @@ class BaseDatabaseIntrospection(object): Returns an unsorted list of names of all tables that exist in the database. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseDatabaseIntrospection may require a get_table_list() method') def django_table_names(self, only_existing=False): """ @@ -1322,7 +1322,7 @@ class BaseDatabaseIntrospection(object): Backends can override this to return a list of (column_name, referenced_table_name, referenced_column_name) for all key columns in given table. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseDatabaseIntrospection may require a get_key_columns() method') def get_primary_key_column(self, cursor, table_name): """ @@ -1342,7 +1342,7 @@ class BaseDatabaseIntrospection(object): Only single-column indexes are introspected. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseDatabaseIntrospection may require a get_indexes() method') def get_constraints(self, cursor, table_name): """ @@ -1361,7 +1361,7 @@ class BaseDatabaseIntrospection(object): Some backends may return special constraint names that don't exist if they don't name constraints of a certain type (e.g. SQLite) """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseDatabaseIntrospection may require a get_constraints() method') class BaseDatabaseClient(object): @@ -1378,7 +1378,7 @@ class BaseDatabaseClient(object): self.connection = connection def runshell(self): - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseDatabaseClient must provide a runshell() method') class BaseDatabaseValidation(object): diff --git a/django/db/backends/schema.py b/django/db/backends/schema.py index 5f0bf0a28b..bc529f47e0 100644 --- a/django/db/backends/schema.py +++ b/django/db/backends/schema.py @@ -148,7 +148,7 @@ class BaseDatabaseSchemaEditor(object): """ Only used for backends which have requires_literal_defaults feature """ - raise NotImplementedError() + raise NotImplementedError('subclasses of BaseDatabaseSchemaEditor for backends which have requires_literal_defaults must provide a prepare_default() method') def effective_default(self, field): """ diff --git a/django/db/migrations/operations/base.py b/django/db/migrations/operations/base.py index dcdb1ad30b..217c6ee843 100644 --- a/django/db/migrations/operations/base.py +++ b/django/db/migrations/operations/base.py @@ -38,14 +38,14 @@ class Operation(object): Takes the state from the previous migration, and mutates it so that it matches what this migration would perform. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of Operation must provide a state_forwards() method') def database_forwards(self, app_label, schema_editor, from_state, to_state): """ Performs the mutation on the database schema in the normal (forwards) direction. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of Operation must provide a database_forwards() method') def database_backwards(self, app_label, schema_editor, from_state, to_state): """ @@ -53,7 +53,7 @@ class Operation(object): direction - e.g. if this were CreateModel, it would in fact drop the model's table. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of Operation must provide a database_backwards() method') def describe(self): """ diff --git a/django/forms/widgets.py b/django/forms/widgets.py index 26f8d312e4..ea701bdcf9 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -190,7 +190,7 @@ class Widget(six.with_metaclass(MediaDefiningClass)): The 'value' given is not guaranteed to be valid input, so subclass implementations should program defensively. """ - raise NotImplementedError + raise NotImplementedError('subclasses of Widget must provide a render() method') def build_attrs(self, extra_attrs=None, **kwargs): "Helper function for building an attribute dictionary." diff --git a/django/template/base.py b/django/template/base.py index 1265040fc0..c6ab7790a0 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -99,7 +99,7 @@ class Origin(object): self.name = name def reload(self): - raise NotImplementedError + raise NotImplementedError('subclasses of Origin must provide a reload() method') def __str__(self): return self.name @@ -385,7 +385,7 @@ class TokenParser(object): """ Overload this method to do the actual parsing and return the result. """ - raise NotImplementedError() + raise NotImplementedError('subclasses of Tokenparser must provide a top() method') def more(self): """ diff --git a/django/template/loader.py b/django/template/loader.py index 44b8f600fb..30d3271960 100644 --- a/django/template/loader.py +++ b/django/template/loader.py @@ -61,7 +61,7 @@ class BaseLoader(object): name. """ - raise NotImplementedError + raise NotImplementedError('subclasses of BaseLoader must provide a load_template_source() method') def reset(self): """ diff --git a/django/utils/archive.py b/django/utils/archive.py index 0faf1fa781..0a95fa84a6 100644 --- a/django/utils/archive.py +++ b/django/utils/archive.py @@ -126,10 +126,10 @@ class BaseArchive(object): return True def extract(self): - raise NotImplementedError + raise NotImplementedError('subclasses of BaseArchive must provide an extract() method') def list(self): - raise NotImplementedError + raise NotImplementedError('subclasses of BaseArchive must provide a list() method') class TarArchive(BaseArchive): diff --git a/django/utils/dateformat.py b/django/utils/dateformat.py index 85eb975f84..3c235d9867 100644 --- a/django/utils/dateformat.py +++ b/django/utils/dateformat.py @@ -65,7 +65,7 @@ class TimeFormat(Formatter): def B(self): "Swatch Internet time" - raise NotImplementedError + raise NotImplementedError('may be implemented in a future release') def e(self): """ diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py index e632845a84..f7fec5e7f9 100644 --- a/django/utils/feedgenerator.py +++ b/django/utils/feedgenerator.py @@ -177,7 +177,7 @@ class SyndicationFeed(object): Outputs the feed in the given encoding to outfile, which is a file-like object. Subclasses should override this. """ - raise NotImplementedError + raise NotImplementedError('subclasses of SyndicationFeed must provide a write() method') def writeString(self, encoding): """ diff --git a/django/utils/functional.py b/django/utils/functional.py index bfd59e5340..65ec4b53c4 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -257,7 +257,7 @@ class LazyObject(object): """ Must be implemented by subclasses to initialise the wrapped object. """ - raise NotImplementedError + raise NotImplementedError('subclasses of LazyObject must provide a _setup() method') # Introspection support __dir__ = new_method_proxy(dir) diff --git a/django/utils/regex_helper.py b/django/utils/regex_helper.py index 7b40d141de..449bb8da42 100644 --- a/django/utils/regex_helper.py +++ b/django/utils/regex_helper.py @@ -92,7 +92,7 @@ def normalize(pattern): result.append(".") elif ch == '|': # FIXME: One day we'll should do this, but not in 1.0. - raise NotImplementedError + raise NotImplementedError('Awaiting Implementation') elif ch == "^": pass elif ch == '$': diff --git a/tests/mail/tests.py b/tests/mail/tests.py index bb57ca37ff..7bef1a3bb3 100644 --- a/tests/mail/tests.py +++ b/tests/mail/tests.py @@ -443,10 +443,10 @@ class BaseEmailBackendTests(HeadersCheckMixin, object): self.assertEqual(first[:len(second)], second, "First string doesn't start with the second.") def get_mailbox_content(self): - raise NotImplementedError + raise NotImplementedError('subclasses of BaseEmailBackendTests must provide a get_mailbox_content() method') def flush_mailbox(self): - raise NotImplementedError + raise NotImplementedError('subclasses of BaseEmailBackendTests may require a flush_mailbox() method') def get_the_message(self): mailbox = self.get_mailbox_content() From 79ccd1a101e6379c5a49da18fc006816e5ed127c Mon Sep 17 00:00:00 2001 From: Aymeric Augustin <aymeric.augustin@m4x.org> Date: Tue, 10 Sep 2013 18:06:43 +0200 Subject: [PATCH 28/58] Fixed test that fails when pytz is installed. pytz' localize() method is the bane of my life. --- tests/utils_tests/test_dateformat.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/utils_tests/test_dateformat.py b/tests/utils_tests/test_dateformat.py index a1980b7930..ebf46f3ee7 100644 --- a/tests/utils_tests/test_dateformat.py +++ b/tests/utils_tests/test_dateformat.py @@ -7,7 +7,7 @@ from django.test import TestCase from django.test.utils import override_settings from django.utils.dateformat import format from django.utils import dateformat -from django.utils.timezone import utc, get_fixed_timezone, get_default_timezone +from django.utils.timezone import utc, get_fixed_timezone, get_default_timezone, make_aware from django.utils import translation @@ -34,16 +34,17 @@ class DateFormatTests(TestCase): def test_datetime_with_local_tzinfo(self): ltz = get_default_timezone() - dt = datetime(2009, 5, 16, 5, 30, 30, tzinfo=ltz) + dt = make_aware(datetime(2009, 5, 16, 5, 30, 30), ltz) self.assertEqual(datetime.fromtimestamp(int(format(dt, 'U')), ltz), dt) self.assertEqual(datetime.fromtimestamp(int(format(dt, 'U'))), dt.replace(tzinfo=None)) def test_datetime_with_tzinfo(self): tz = get_fixed_timezone(-510) ltz = get_default_timezone() - dt = datetime(2009, 5, 16, 5, 30, 30, tzinfo=tz) + dt = make_aware(datetime(2009, 5, 16, 5, 30, 30), ltz) self.assertEqual(datetime.fromtimestamp(int(format(dt, 'U')), tz), dt) self.assertEqual(datetime.fromtimestamp(int(format(dt, 'U')), ltz), dt) + # astimezone() is safe here because the target timezone doesn't have DST self.assertEqual(datetime.fromtimestamp(int(format(dt, 'U'))), dt.astimezone(ltz).replace(tzinfo=None)) self.assertEqual(datetime.fromtimestamp(int(format(dt, 'U')), tz).utctimetuple(), dt.utctimetuple()) self.assertEqual(datetime.fromtimestamp(int(format(dt, 'U')), ltz).utctimetuple(), dt.utctimetuple()) From 4840fd9cbc3987781f7e6fab2520c9bd42aec057 Mon Sep 17 00:00:00 2001 From: Juan Catalano <catalanojuan@gmail.com> Date: Sat, 7 Sep 2013 18:13:57 -0300 Subject: [PATCH 29/58] Fixed #20919 -- Extended assertRedirects to be able to avoid fetching redirect's response. Thanks mjtamlyn for the suggestion. --- django/test/testcases.py | 21 ++++++++++++--------- docs/releases/1.7.txt | 5 +++++ docs/topics/testing/overview.txt | 8 +++++++- tests/test_client/tests.py | 4 ++++ tests/test_client/urls.py | 3 ++- tests/test_client/views.py | 3 +++ 6 files changed, 33 insertions(+), 11 deletions(-) diff --git a/django/test/testcases.py b/django/test/testcases.py index ab658a04fa..b90fafaf50 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -225,12 +225,14 @@ class SimpleTestCase(unittest.TestCase): return override_settings(**kwargs) def assertRedirects(self, response, expected_url, status_code=302, - target_status_code=200, host=None, msg_prefix=''): + target_status_code=200, host=None, msg_prefix='', + fetch_redirect_response=True): """Asserts that a response redirected to a specific URL, and that the redirect URL can be loaded. Note that assertRedirects won't work for external links since it uses - TestClient to do a request. + TestClient to do a request (use fetch_redirect_response=False to check + such links without fetching thtem). """ if msg_prefix: msg_prefix += ": " @@ -264,14 +266,15 @@ class SimpleTestCase(unittest.TestCase): url = response.url scheme, netloc, path, query, fragment = urlsplit(url) - redirect_response = response.client.get(path, QueryDict(query)) + if fetch_redirect_response: + redirect_response = response.client.get(path, QueryDict(query)) - # Get the redirection page, using the same client that was used - # to obtain the original response. - self.assertEqual(redirect_response.status_code, target_status_code, - msg_prefix + "Couldn't retrieve redirection page '%s':" - " response code was %d (expected %d)" % - (path, redirect_response.status_code, target_status_code)) + # Get the redirection page, using the same client that was used + # to obtain the original response. + self.assertEqual(redirect_response.status_code, target_status_code, + msg_prefix + "Couldn't retrieve redirection page '%s':" + " response code was %d (expected %d)" % + (path, redirect_response.status_code, target_status_code)) e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit( expected_url) diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 9f05055fd2..3e247fd211 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -293,6 +293,11 @@ Tests :attr:`~django.test.runner.DiscoverRunner.test_runner`, which facilitate overriding the way tests are collected and run. +* The ``fetch_redirect_response`` argument was added to + :meth:`~django.test.SimpleTestCase.assertRedirects`. Since the test + client can't fetch externals URLs, this allows you to use ``assertRedirects`` + with redirects that aren't part of your Django app. + Backwards incompatible changes in 1.7 ===================================== diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index 7c6f5caa47..420401cd4a 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -1542,7 +1542,7 @@ your test suite. You can use this as a context manager in the same way as :meth:`~SimpleTestCase.assertTemplateUsed`. -.. method:: SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='') +.. method:: SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='', fetch_redirect_response=True) Asserts that the response return a ``status_code`` redirect status, it redirected to ``expected_url`` (including any GET data), and the final @@ -1552,6 +1552,12 @@ your test suite. ``target_status_code`` will be the url and status code for the final point of the redirect chain. + .. versionadded:: 1.7 + + If ``fetch_redirect_response`` is ``False``, the final page won't be + loaded. Since the test client can't fetch externals URLs, this is + particularly useful if ``expected_url`` isn't part of your Django app. + .. method:: SimpleTestCase.assertHTMLEqual(html1, html2, msg=None) Asserts that the strings ``html1`` and ``html2`` are equal. The comparison diff --git a/tests/test_client/tests.py b/tests/test_client/tests.py index 85637b982e..6525882663 100644 --- a/tests/test_client/tests.py +++ b/tests/test_client/tests.py @@ -405,6 +405,10 @@ class ClientTest(TestCase): # TODO: Log in with right permissions and request the page again + def test_external_redirect(self): + response = self.client.get('/test_client/django_project_redirect/') + self.assertRedirects(response, 'https://www.djangoproject.com/', fetch_redirect_response=False) + def test_session_modifying_view(self): "Request a page that modifies the session" # Session value isn't set initially diff --git a/tests/test_client/urls.py b/tests/test_client/urls.py index 4d2f4fb86e..693c7de2e1 100644 --- a/tests/test_client/urls.py +++ b/tests/test_client/urls.py @@ -29,5 +29,6 @@ urlpatterns = patterns('', (r'^session_view/$', views.session_view), (r'^broken_view/$', views.broken_view), (r'^mail_sending_view/$', views.mail_sending_view), - (r'^mass_mail_sending_view/$', views.mass_mail_sending_view) + (r'^mass_mail_sending_view/$', views.mass_mail_sending_view), + (r'^django_project_redirect/$', views.django_project_redirect), ) diff --git a/tests/test_client/views.py b/tests/test_client/views.py index 08cdd8c198..35492238b8 100644 --- a/tests/test_client/views.py +++ b/tests/test_client/views.py @@ -257,3 +257,6 @@ def mass_mail_sending_view(request): c.send_messages([m1,m2]) return HttpResponse("Mail sent") + +def django_project_redirect(request): + return HttpResponseRedirect('https://www.djangoproject.com/') From 053de6131af83c63ec17d38578889c71de913d24 Mon Sep 17 00:00:00 2001 From: e0ne <e0ne@e0ne.info> Date: Tue, 10 Sep 2013 16:09:55 +0300 Subject: [PATCH 30/58] Fixed #5749 -- Added field_name as a key in the _html_output dict Thanks SmileyChris for the suggestion. --- django/forms/forms.py | 3 ++- tests/forms_tests/tests/test_forms.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/django/forms/forms.py b/django/forms/forms.py index d8d08e18fe..e04d6a0781 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -185,7 +185,8 @@ class BaseForm(object): 'label': force_text(label), 'field': six.text_type(bf), 'help_text': help_text, - 'html_class_attr': html_class_attr + 'html_class_attr': html_class_attr, + 'field_name': bf.html_name, }) if top_errors: diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index 168bc1de6a..c9dbece80f 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -1950,3 +1950,14 @@ class FormsTestCase(TestCase): boundfield = SomeForm(label_suffix='!')['field'] self.assertHTMLEqual(boundfield.label_tag(label_suffix='$'), '<label for="id_field">Field$</label>') + + def test_field_name(self): + """#5749 - `field_name` may be used as a key in _html_output().""" + class SomeForm(Form): + some_field = CharField() + + def as_p(self): + return self._html_output(u'<p id="p_%(field_name)s"></p>', u'%s', '</p>', u' %s', True) + + form = SomeForm() + self.assertHTMLEqual(form.as_p(), '<p id="p_some_field"></p>') From d5d0e03ec826e0664f1c9b8b042497f7f5219b0c Mon Sep 17 00:00:00 2001 From: Florian Apolloner <florian@apolloner.eu> Date: Tue, 10 Sep 2013 19:17:01 +0200 Subject: [PATCH 31/58] Fixed test errors from 053de6131af83c63ec17d38578889c71de913d24 on py3.2. --- tests/forms_tests/tests/test_forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index c9dbece80f..3204ec9437 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -1957,7 +1957,7 @@ class FormsTestCase(TestCase): some_field = CharField() def as_p(self): - return self._html_output(u'<p id="p_%(field_name)s"></p>', u'%s', '</p>', u' %s', True) + return self._html_output('<p id="p_%(field_name)s"></p>', '%s', '</p>', ' %s', True) form = SomeForm() self.assertHTMLEqual(form.as_p(), '<p id="p_some_field"></p>') From 4e96dac450e3546bf86220932f5a64fea1ad5bac Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Tue, 10 Sep 2013 13:56:49 -0400 Subject: [PATCH 32/58] Fixed #19298 -- Added MultiValueField.__deepcopy__ Thanks nick.phillips at otago.ac.nz for the report. --- django/forms/fields.py | 5 +++++ tests/forms_tests/tests/test_forms.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/django/forms/fields.py b/django/forms/fields.py index 7a59a3d664..80550c232b 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -977,6 +977,11 @@ class MultiValueField(Field): f.required = False self.fields = fields + def __deepcopy__(self, memo): + result = super(MultiValueField, self).__deepcopy__(memo) + result.fields = tuple([x.__deepcopy__(memo) for x in self.fields]) + return result + def validate(self, value): pass diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index 3204ec9437..640f48fe44 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import copy import datetime from django.core.files.uploadedfile import SimpleUploadedFile @@ -1793,6 +1794,26 @@ class FormsTestCase(TestCase): self.assertTrue(form.is_valid()) self.assertEqual(form.cleaned_data, {'name' : 'fname lname'}) + def test_multivalue_deep_copy(self): + """ + #19298 -- MultiValueField needs to override the default as it needs + to deep-copy subfields: + """ + class ChoicesField(MultiValueField): + def __init__(self, fields=(), *args, **kwargs): + fields = (ChoiceField(label='Rank', + choices=((1,1),(2,2))), + CharField(label='Name', max_length=10)) + super(ChoicesField, self).__init__(fields=fields, *args, **kwargs) + + + field = ChoicesField() + field2 = copy.deepcopy(field) + self.assertTrue(isinstance(field2, ChoicesField)) + self.assertFalse(id(field2.fields) == id(field.fields)) + self.assertFalse(id(field2.fields[0].choices) == + id(field.fields[0].choices)) + def test_multivalue_optional_subfields(self): class PhoneField(MultiValueField): def __init__(self, *args, **kwargs): From 8165c2cfd1922bb9d56a9e68e6456736acacf445 Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Tue, 10 Sep 2013 14:23:31 -0400 Subject: [PATCH 33/58] Improved deprecation warning for change in form boolean values. refs #20684 Thanks jacob, jcd, and shai for the suggestions. --- django/forms/util.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/django/forms/util.py b/django/forms/util.py index 320a74e721..bb5bb8c2e4 100644 --- a/django/forms/util.py +++ b/django/forms/util.py @@ -23,11 +23,18 @@ def flatatt(attrs): The result is passed through 'mark_safe'. """ - if [v for v in attrs.values() if v is True or v is False]: - warnings.warn( - 'The meaning of boolean values for widget attributes will change in Django 1.8', - DeprecationWarning - ) + for attr_name, value in attrs.items(): + if type(value) is bool: + warnings.warn( + "In Django 1.8, widget attribute %(attr_name)s=%(bool_value)s " + "will %(action)s. To preserve current behavior, use the " + "string '%(bool_value)s' instead of the boolean value." % { + 'attr_name': attr_name, + 'action': "be rendered as '%s'" % attr_name if value else "not be rendered", + 'bool_value': value, + }, + DeprecationWarning + ) return format_html_join('', ' {0}="{1}"', sorted(attrs.items())) @python_2_unicode_compatible From 751dc0a36b384024377a7bd1254ae110af313ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarjei=20Hus=C3=B8y?= <thusoy@users.noreply.github.com> Date: Tue, 10 Sep 2013 22:10:00 +0200 Subject: [PATCH 34/58] Fix broken sphinx reference to staticfiles. --- docs/howto/static-files/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/static-files/index.txt b/docs/howto/static-files/index.txt index 5702091053..e32a752454 100644 --- a/docs/howto/static-files/index.txt +++ b/docs/howto/static-files/index.txt @@ -106,7 +106,7 @@ this by adding the following snippet to your urls.py:: Also this helper function only serves the actual :setting:`STATIC_ROOT` folder; it doesn't perform static files discovery like - `:mod:`django.contrib.staticfiles`. + :mod:`django.contrib.staticfiles`. Serving files uploaded by a user during development. ==================================================== From 7fe5b656c9d4f54d70b83edaa6225115805a2325 Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Tue, 27 Aug 2013 18:50:11 -0400 Subject: [PATCH 35/58] Prevented arbitrary file inclusion with {% ssi %} tag and relative paths. Thanks Rainer Koirikivi for the report and draft patch. This is a security fix; disclosure to follow shortly. --- django/template/defaulttags.py | 2 ++ tests/template_tests/tests.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 0eae48eb87..798323c2cf 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -1,6 +1,7 @@ """Default tags used by the template system, available to all templates.""" from __future__ import unicode_literals +import os import sys import re from datetime import datetime @@ -328,6 +329,7 @@ class RegroupNode(Node): return '' def include_is_allowed(filepath): + filepath = os.path.abspath(filepath) for root in settings.ALLOWED_INCLUDE_ROOTS: if filepath.startswith(root): return True diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index 7fb194fe38..2551d43890 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -1902,3 +1902,34 @@ class RequestContextTests(unittest.TestCase): # The stack should now contain 3 items: # [builtins, supplied context, context processor] self.assertEqual(len(ctx.dicts), 3) + + +class SSITests(TestCase): + def setUp(self): + self.this_dir = os.path.dirname(os.path.abspath(upath(__file__))) + self.ssi_dir = os.path.join(self.this_dir, "templates", "first") + + def render_ssi(self, path): + # the path must exist for the test to be reliable + self.assertTrue(os.path.exists(path)) + return template.Template('{%% ssi "%s" %%}' % path).render(Context()) + + def test_allowed_paths(self): + acceptable_path = os.path.join(self.ssi_dir, "..", "first", "test.html") + with override_settings(ALLOWED_INCLUDE_ROOTS=(self.ssi_dir,)): + self.assertEqual(self.render_ssi(acceptable_path), 'First template\n') + + def test_relative_include_exploit(self): + """ + May not bypass ALLOWED_INCLUDE_ROOTS with relative paths + + e.g. if ALLOWED_INCLUDE_ROOTS = ("/var/www",), it should not be + possible to do {% ssi "/var/www/../../etc/passwd" %} + """ + disallowed_paths = [ + os.path.join(self.ssi_dir, "..", "ssi_include.html"), + os.path.join(self.ssi_dir, "..", "second", "test.html"), + ] + with override_settings(ALLOWED_INCLUDE_ROOTS=(self.ssi_dir,)): + for path in disallowed_paths: + self.assertEqual(self.render_ssi(path), '') From baec6a26dd259a0b41f59fa123f7675d8e05de61 Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Fri, 23 Aug 2013 06:49:37 -0400 Subject: [PATCH 36/58] Added 1.4.7/1.5.3 release notes --- docs/releases/1.4.7.txt | 25 +++++++++++++++++++++ docs/releases/1.5.3.txt | 50 +++++++++++++++++++++++++++++++++++++++++ docs/releases/index.txt | 2 ++ 3 files changed, 77 insertions(+) create mode 100644 docs/releases/1.4.7.txt create mode 100644 docs/releases/1.5.3.txt diff --git a/docs/releases/1.4.7.txt b/docs/releases/1.4.7.txt new file mode 100644 index 0000000000..64d308894c --- /dev/null +++ b/docs/releases/1.4.7.txt @@ -0,0 +1,25 @@ +========================== +Django 1.4.7 release notes +========================== + +*September 10, 2013* + +Django 1.4.7 fixes one security issue present in previous Django releases in +the 1.4 series. + +Directory traversal vulnerability in :ttag:`ssi` template tag +------------------------------------------------------------- + +In previous versions of Django it was possible to bypass the +:setting:`ALLOWED_INCLUDE_ROOTS` setting used for security with the :ttag:`ssi` +template tag by specifying a relative path that starts with one of the allowed +roots. For example, if ``ALLOWED_INCLUDE_ROOTS = ("/var/www",)`` the following +would be possible: + +.. code-block:: html+django + + {% ssi "/var/www/../../etc/passwd" %} + +In practice this is not a very common problem, as it would require the template +author to put the :ttag:`ssi` file in a user-controlled variable, but it's +possible in principle. diff --git a/docs/releases/1.5.3.txt b/docs/releases/1.5.3.txt new file mode 100644 index 0000000000..bdf68d5621 --- /dev/null +++ b/docs/releases/1.5.3.txt @@ -0,0 +1,50 @@ +========================== +Django 1.5.3 release notes +========================== + +*September 10, 2013* + +This is Django 1.5.3, the third release in the Django 1.5 series. It addresses +one security issue and also contains an opt-in feature to enhance the security +of :mod:`django.contrib.sessions`. + +Directory traversal vulnerability in :ttag:`ssi` template tag +------------------------------------------------------------- + +In previous versions of Django it was possible to bypass the +:setting:`ALLOWED_INCLUDE_ROOTS` setting used for security with the :ttag:`ssi` +template tag by specifying a relative path that starts with one of the allowed +roots. For example, if ``ALLOWED_INCLUDE_ROOTS = ("/var/www",)`` the following +would be possible: + +.. code-block:: html+django + + {% ssi "/var/www/../../etc/passwd" %} + +In practice this is not a very common problem, as it would require the template +author to put the :ttag:`ssi` file in a user-controlled variable, but it's +possible in principle. + +Mitigating a remote-code execution vulnerability in :mod:`django.contrib.sessions` +---------------------------------------------------------------------------------- + +:mod:`django.contrib.sessions` currently uses :mod:`pickle` to serialize +session data before storing it in the backend. If you're using the :ref:`signed +cookie session backend<cookie-session-backend>` and :setting:`SECRET_KEY` is +known by an attacker (there isn't an inherent vulnerability in Django that +would cause it to leak), the attacker could insert a string into his session +which, when unpickled, executes arbitrary code on the server. The technique for +doing so is simple and easily available on the internet. Although the cookie +session storage signs the cookie-stored data to prevent tampering, a +:setting:`SECRET_KEY` leak immediately escalates to a remote code execution +vulnerability. + +This attack can be mitigated by serializing session data using JSON rather +than :mod:`pickle`. To facilitate this, Django 1.5.3 introduces a new setting, +:setting:`SESSION_SERIALIZER`, to customize the session serialization format. +For backwards compatibility, this setting defaults to using :mod:`pickle`. +While JSON serialization does not support all Python objects like :mod:`pickle` +does, we highly recommend switching to JSON-serialized values. Also, +as JSON requires string keys, you will likely run into problems if you are +using non-string keys in ``request.session``. See the +:ref:`session_serialization` documentation for more details. diff --git a/docs/releases/index.txt b/docs/releases/index.txt index c3ba5cf478..1b0d1a371d 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -36,6 +36,7 @@ Final releases .. toctree:: :maxdepth: 1 + 1.5.3 1.5.2 1.5.1 1.5 @@ -45,6 +46,7 @@ Final releases .. toctree:: :maxdepth: 1 + 1.4.7 1.4.6 1.4.5 1.4.4 From 5eca021d48cc388d89347d01cb7bfafc9fcfa331 Mon Sep 17 00:00:00 2001 From: Kevin Christopher Henry <k@severian.com> Date: Tue, 10 Sep 2013 18:00:36 -0400 Subject: [PATCH 37/58] Documentation -- Improved description of cache arguments - Fixed some grammar and formatting mistakes - Added the type and default for CULL_FREQUENCY - Made the note on culling the entire cache more precise. (It's actually slower on the filesystem backend.) --- docs/topics/cache.txt | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index 092df1f876..60c168c2c1 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -341,40 +341,37 @@ been well-tested and are easy to use. Cache arguments --------------- -In addition to the defining the engine and name of the each cache -backend, each cache backend can be given additional arguments to -control caching behavior. These arguments are provided as additional -keys in the :setting:`CACHES` setting. Valid arguments are as follows: +Each cache backend can be given additional arguments to control caching +behavior. These arguments are provided as additional keys in the +:setting:`CACHES` setting. Valid arguments are as follows: * :setting:`TIMEOUT <CACHES-TIMEOUT>`: The default timeout, in - seconds, to use for the cache. This argument defaults to 300 + seconds, to use for the cache. This argument defaults to ``300`` seconds (5 minutes). * :setting:`OPTIONS <CACHES-OPTIONS>`: Any options that should be - passed to cache backend. The list options understood by each - backend vary with each backend. + passed to the cache backend. The list of valid options will vary + with each backend, and cache backends backed by a third-party library + will pass their options directly to the underlying cache library. Cache backends that implement their own culling strategy (i.e., the ``locmem``, ``filesystem`` and ``database`` backends) will honor the following options: - * ``MAX_ENTRIES``: the maximum number of entries allowed in + * ``MAX_ENTRIES``: The maximum number of entries allowed in the cache before old values are deleted. This argument defaults to ``300``. * ``CULL_FREQUENCY``: The fraction of entries that are culled when ``MAX_ENTRIES`` is reached. The actual ratio is - ``1/CULL_FREQUENCY``, so set ``CULL_FREQUENCY``: to ``2`` to - cull half of the entries when ``MAX_ENTRIES`` is reached. + ``1 / CULL_FREQUENCY``, so set ``CULL_FREQUENCY`` to ``2`` to + cull half the entries when ``MAX_ENTRIES`` is reached. This argument + should be an integer and defaults to ``3``. A value of ``0`` for ``CULL_FREQUENCY`` means that the entire cache will be dumped when ``MAX_ENTRIES`` is reached. - This makes culling *much* faster at the expense of more - cache misses. - - Cache backends backed by a third-party library will pass their - options directly to the underlying cache library. As a result, - the list of valid options depends on the library in use. + On some backends (``database`` in particular) this makes culling *much* + faster at the expense of more cache misses. * :setting:`KEY_PREFIX <CACHES-KEY_PREFIX>`: A string that will be automatically included (prepended by default) to all cache keys From da843e7dba4ae8ed2846475564bb6ded82960827 Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Wed, 11 Sep 2013 08:17:15 -0400 Subject: [PATCH 38/58] Fixed #20887 -- Added a warning to GzipMiddleware in light of BREACH. Thanks EvilDMP for the report and Russell Keith-Magee for the draft text. --- docs/ref/middleware.txt | 14 ++++++++++++++ docs/topics/cache.txt | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index d011f054ac..7aac286c2d 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -79,6 +79,20 @@ GZip middleware .. class:: GZipMiddleware +.. warning:: + + Security researchers recently revealed that when compression techniques + (including ``GZipMiddleware``) are used on a website, the site becomes + exposed to a number of possible attacks. These approaches can be used to + compromise, amongst other things, Django's CSRF protection. Before using + ``GZipMiddleware`` on your site, you should consider very carefully whether + you are subject to these attacks. If you're in *any* doubt about whether + you're affected, you should avoid using ``GZipMiddleware``. For more + details, see the `the BREACH paper (PDF)`_ and `breachattack.com`_. + + .. _the BREACH paper (PDF): http://breachattack.com/resources/BREACH%20-%20SSL,%20gone%20in%2030%20seconds.pdf + .. _breachattack.com: http://breachattack.com + Compresses content for browsers that understand GZip compression (all modern browsers). diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index 60c168c2c1..2e388712d9 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -1173,7 +1173,10 @@ site's performance: and ``Last-Modified`` headers. * :class:`django.middleware.gzip.GZipMiddleware` compresses responses for all - modern browsers, saving bandwidth and transfer time. + modern browsers, saving bandwidth and transfer time. Be warned, however, + that compression techniques like ``GZipMiddleware`` are subject to attacks. + See the warning in :class:`~django.middleware.gzip.GZipMiddleware` for + details. Order of MIDDLEWARE_CLASSES =========================== From 170f72136758add6c9c0c59240cfce73d5672cc2 Mon Sep 17 00:00:00 2001 From: Keryn Knight <keryn@kerynknight.com> Date: Sat, 7 Sep 2013 14:20:04 +0100 Subject: [PATCH 39/58] Fixed #21056 -- AdminSite.app_index no longer blindly accepts any app-labelish input. --- django/contrib/admin/sites.py | 15 +++++++++++++-- tests/admin_views/tests.py | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index bb767a6dfa..2dac947fbc 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -232,14 +232,25 @@ class AdminSite(object): url(r'^password_change/done/$', wrap(self.password_change_done, cacheable=True), name='password_change_done'), url(r'^jsi18n/$', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'), url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$', wrap(contenttype_views.shortcut), name='view_on_site'), - url(r'^(?P<app_label>\w+)/$', wrap(self.app_index), name='app_list'), ) - # Add in each model's views. + # Add in each model's views, and create a list of valid URLS for the + # app_index + valid_app_labels = [] for model, model_admin in six.iteritems(self._registry): urlpatterns += patterns('', url(r'^%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)) ) + if model._meta.app_label not in valid_app_labels: + valid_app_labels.append(model._meta.app_label) + + # If there were ModelAdmins registered, we should have a list of app + # labels for which we need to allow access to the app_index view, + if valid_app_labels: + regex = r'^(?P<app_label>' + '|'.join(valid_app_labels) + ')/$' + urlpatterns += patterns('', + url(regex, wrap(self.app_index), name='app_list'), + ) return urlpatterns @property diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 19bf00f302..75e8a51acd 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -9,7 +9,7 @@ import unittest from django.conf import settings, global_settings from django.core import mail from django.core.files import temp as tempfile -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse, NoReverseMatch # Register auth models with the admin. from django.contrib.auth import get_permission_codename from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME @@ -640,6 +640,20 @@ class AdminViewBasicTest(AdminViewBasicTestCase): # Check the format of the shown object -- shouldn't contain a change link self.assertContains(response, '<th class="field-__str__">UnchangeableObject object</th>', html=True) + def test_invalid_appindex_url(self): + """ + #21056 -- URL reversing shouldn't work for nonexistent apps. + """ + good_url = '/test_admin/admin/admin_views/' + confirm_good_url = reverse('admin:app_list', + kwargs={'app_label': 'admin_views'}) + self.assertEqual(good_url, confirm_good_url) + + with self.assertRaises(NoReverseMatch): + reverse('admin:app_list', kwargs={'app_label': 'this_should_fail'}) + with self.assertRaises(NoReverseMatch): + reverse('admin:app_list', args=('admin_views2',)) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewFormUrlTest(TestCase): From abb10db06fb2ecb3e897462ec72417d10b39b8a4 Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon <bmispelon@gmail.com> Date: Wed, 11 Sep 2013 15:20:15 +0200 Subject: [PATCH 40/58] Fixed #21089 -- Allow TransactionTestcase subclasses to define an empty list of fixtures. Thanks to lgs for the report and initial patch. --- django/test/testcases.py | 7 +++++-- tests/fixtures/tests.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/django/test/testcases.py b/django/test/testcases.py index b90fafaf50..bc73496801 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -699,6 +699,9 @@ class TransactionTestCase(SimpleTestCase): # Subclasses can enable only a subset of apps for faster tests available_apps = None + # Subclasses can define fixtures which will be automatically installed. + fixtures = None + def _pre_setup(self): """Performs any pre-test setup. This includes: @@ -746,7 +749,7 @@ class TransactionTestCase(SimpleTestCase): if self.reset_sequences: self._reset_sequences(db_name) - if hasattr(self, 'fixtures'): + if self.fixtures: # We have to use this slightly awkward syntax due to the fact # that we're using *args and **kwargs together. call_command('loaddata', *self.fixtures, @@ -838,7 +841,7 @@ class TestCase(TransactionTestCase): disable_transaction_methods() for db_name in self._databases_names(include_mirrors=False): - if hasattr(self, 'fixtures'): + if self.fixtures: try: call_command('loaddata', *self.fixtures, **{ diff --git a/tests/fixtures/tests.py b/tests/fixtures/tests.py index c24e0806fa..a1ecf007ce 100644 --- a/tests/fixtures/tests.py +++ b/tests/fixtures/tests.py @@ -24,6 +24,17 @@ class TestCaseFixtureLoadingTests(TestCase): ]) +class SubclassTestCaseFixtureLoadingTests(TestCaseFixtureLoadingTests): + """ + Make sure that subclasses can remove fixtures from parent class (#21089). + """ + fixtures = [] + + def testClassFixtures(self): + "Check that there were no fixture objects installed" + self.assertEqual(Article.objects.count(), 0) + + class DumpDataAssertMixin(object): def _dumpdata_assert(self, args, output, format='json', natural_keys=False, From e61cc87129727c66120b67c376feda3533544db1 Mon Sep 17 00:00:00 2001 From: Michael Manfre <mmanfre@gmail.com> Date: Wed, 11 Sep 2013 11:12:56 -0400 Subject: [PATCH 41/58] Fixed #21090 -- Allowed backends to provide dotted field path to inspectdb. --- django/core/management/commands/inspectdb.py | 7 +++++- tests/inspectdb/tests.py | 24 +++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py index 2cfea028ec..b4c116acab 100644 --- a/django/core/management/commands/inspectdb.py +++ b/django/core/management/commands/inspectdb.py @@ -117,7 +117,12 @@ class Command(NoArgsCommand): if not field_type in ('TextField(', 'CharField('): extra_params['null'] = True - field_desc = '%s = models.%s' % (att_name, field_type) + field_desc = '%s = %s%s' % ( + att_name, + # Custom fields will have a dotted path + '' if '.' in field_type else 'models.', + field_type, + ) if extra_params: if not field_desc.endswith('('): field_desc += ', ' diff --git a/tests/inspectdb/tests.py b/tests/inspectdb/tests.py index c9093b9e9e..d6f6e478e4 100644 --- a/tests/inspectdb/tests.py +++ b/tests/inspectdb/tests.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import re -from unittest import expectedFailure +from unittest import expectedFailure, skipUnless from django.core.management import call_command from django.db import connection @@ -162,3 +162,25 @@ class InspectDBTestCase(TestCase): output = out.getvalue() self.longMessage = False self.assertIn(" managed = False", output, msg='inspectdb should generate unmanaged models.') + + @skipUnless(connection.vendor == 'sqlite', + "Only patched sqlite's DatabaseIntrospection.data_types_reverse for this test") + def test_custom_fields(self): + """ + Introspection of columns with a custom field (#21090) + """ + out = StringIO() + orig_data_types_reverse = connection.introspection.data_types_reverse + try: + connection.introspection.data_types_reverse = { + 'text': 'myfields.TextField', + 'bigint': 'BigIntegerField', + } + call_command('inspectdb', + table_name_filter=lambda tn: tn.startswith('inspectdb_columntypes'), + stdout=out) + output = out.getvalue() + self.assertIn("text_field = myfields.TextField()", output) + self.assertIn("big_int_field = models.BigIntegerField()", output) + finally: + connection.introspection.data_types_reverse = orig_data_types_reverse From bd72c2acb60a7adeb0334a823063d6eab9206081 Mon Sep 17 00:00:00 2001 From: Phaneendra Chiruvella <hi@pcx.io> Date: Thu, 12 Sep 2013 03:15:00 +0530 Subject: [PATCH 42/58] Minor typo fix in django.contrib.auth.models.User docs --- docs/ref/contrib/auth.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt index 799d3277ab..154c0ab75b 100644 --- a/docs/ref/contrib/auth.txt +++ b/docs/ref/contrib/auth.txt @@ -142,7 +142,7 @@ Methods .. versionchanged:: 1.6 In Django 1.4 and 1.5, a blank string was unintentionally stored - as an unsable password. + as an unusable password. .. method:: check_password(raw_password) From c82f6c2227f1b86df8726293e8d6d59449b0ab93 Mon Sep 17 00:00:00 2001 From: Claude Paroz <claude@2xlibre.net> Date: Thu, 12 Sep 2013 10:30:45 +0200 Subject: [PATCH 43/58] Add a test for the geo-enabled inspectdb command --- django/contrib/gis/tests/inspectapp/models.py | 1 + django/contrib/gis/tests/inspectapp/tests.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/django/contrib/gis/tests/inspectapp/models.py b/django/contrib/gis/tests/inspectapp/models.py index 0f1b0d4e61..e4c0f542a9 100644 --- a/django/contrib/gis/tests/inspectapp/models.py +++ b/django/contrib/gis/tests/inspectapp/models.py @@ -9,5 +9,6 @@ class AllOGRFields(models.Model): f_datetime = models.DateTimeField() f_time = models.TimeField() geom = models.PolygonField() + point = models.PointField() objects = models.GeoManager() diff --git a/django/contrib/gis/tests/inspectapp/tests.py b/django/contrib/gis/tests/inspectapp/tests.py index 4b7c5ff21d..ea4ed3caca 100644 --- a/django/contrib/gis/tests/inspectapp/tests.py +++ b/django/contrib/gis/tests/inspectapp/tests.py @@ -3,11 +3,13 @@ from __future__ import unicode_literals import os from unittest import skipUnless +from django.core.management import call_command from django.db import connections from django.test import TestCase from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.geometry.test_data import TEST_DATA from django.contrib.gis.tests.utils import HAS_SPATIAL_DB +from django.utils.six import StringIO if HAS_GDAL: from django.contrib.gis.gdal import Driver @@ -16,6 +18,22 @@ if HAS_GDAL: from .models import AllOGRFields +@skipUnless(HAS_GDAL and HAS_SPATIAL_DB, "GDAL and spatial db are required.") +class InspectDbTests(TestCase): + def test_geom_columns(self): + """ + Test the geo-enabled inspectdb command. + """ + out = StringIO() + call_command('inspectdb', + table_name_filter=lambda tn:tn.startswith('inspectapp_'), + stdout=out) + output = out.getvalue() + self.assertIn('geom = models.PolygonField()', output) + self.assertIn('point = models.PointField()', output) + self.assertIn('objects = models.GeoManager()', output) + + @skipUnless(HAS_GDAL and HAS_SPATIAL_DB, "GDAL and spatial db are required.") class OGRInspectTest(TestCase): maxDiff = 1024 From 4c5641dd92e5cfa0223f417f30d109ff998b8041 Mon Sep 17 00:00:00 2001 From: Claude Paroz <claude@2xlibre.net> Date: Thu, 12 Sep 2013 11:14:16 +0200 Subject: [PATCH 44/58] Fixed inspectdb test for spatialite >=4 --- django/contrib/gis/db/backends/spatialite/introspection.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/django/contrib/gis/db/backends/spatialite/introspection.py b/django/contrib/gis/db/backends/spatialite/introspection.py index 4f12ade114..90ed83fa08 100644 --- a/django/contrib/gis/db/backends/spatialite/introspection.py +++ b/django/contrib/gis/db/backends/spatialite/introspection.py @@ -25,9 +25,10 @@ class SpatiaLiteIntrospection(DatabaseIntrospection): cursor = self.connection.cursor() try: # Querying the `geometry_columns` table to get additional metadata. - cursor.execute('SELECT "coord_dimension", "srid", "type" ' - 'FROM "geometry_columns" ' - 'WHERE "f_table_name"=%s AND "f_geometry_column"=%s', + type_col = 'type' if self.connection.ops.spatial_version < (4, 0, 0) else 'geometry_type' + cursor.execute('SELECT coord_dimension, srid, %s ' + 'FROM geometry_columns ' + 'WHERE f_table_name=%%s AND f_geometry_column=%%s' % type_col, (table_name, geo_col)) row = cursor.fetchone() if not row: From 018037736fe575307f5331ebb4b126a0c886ecef Mon Sep 17 00:00:00 2001 From: Michael Manfre <mmanfre@gmail.com> Date: Thu, 12 Sep 2013 14:32:23 -0400 Subject: [PATCH 45/58] Fixed #21099 - Skip DistinctOnTests unless backend can_distinct_on_fields --- tests/distinct_on_fields/tests.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/distinct_on_fields/tests.py b/tests/distinct_on_fields/tests.py index e9f08df84e..ec6c2d0463 100644 --- a/tests/distinct_on_fields/tests.py +++ b/tests/distinct_on_fields/tests.py @@ -6,6 +6,7 @@ from django.test.utils import str_prefix from .models import Tag, Celebrity, Fan, Staff, StaffTag +@skipUnlessDBFeature('can_distinct_on_fields') class DistinctOnTests(TestCase): def setUp(self): t1 = Tag.objects.create(name='t1') @@ -29,7 +30,6 @@ class DistinctOnTests(TestCase): self.fan2 = Fan.objects.create(fan_of=celeb1) self.fan3 = Fan.objects.create(fan_of=celeb2) - @skipUnlessDBFeature('can_distinct_on_fields') def test_basic_distinct_on(self): """QuerySet.distinct('field', ...) works""" # (qset, expected) tuples @@ -101,7 +101,6 @@ class DistinctOnTests(TestCase): c2 = c1.distinct('pk') self.assertNotIn('OUTER JOIN', str(c2.query)) - @skipUnlessDBFeature('can_distinct_on_fields') def test_distinct_not_implemented_checks(self): # distinct + annotate not allowed with self.assertRaises(NotImplementedError): From 8b366a50f47443947debc327ae9950e3119e05ca Mon Sep 17 00:00:00 2001 From: Ramiro Morales <cramm0@gmail.com> Date: Thu, 12 Sep 2013 19:34:22 -0300 Subject: [PATCH 46/58] Fixed a couple of typos in GeoDjango docs. --- docs/ref/contrib/gis/install/spatialite.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/ref/contrib/gis/install/spatialite.txt b/docs/ref/contrib/gis/install/spatialite.txt index b6cf1943ae..ab40600205 100644 --- a/docs/ref/contrib/gis/install/spatialite.txt +++ b/docs/ref/contrib/gis/install/spatialite.txt @@ -56,7 +56,7 @@ needs to be customized so that SQLite knows to build the R*Tree module:: __ http://www.sqlite.org/rtree.html __ http://www.sqlite.org/download.html -.. _spatialitebuild : +.. _spatialitebuild: SpatiaLite library (``libspatialite``) and tools (``spatialite``) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -74,15 +74,17 @@ customization of the ``configure`` command is necessary. If not, then run the ``configure`` script, make, and install for the SpatiaLite library:: $ cd libspatialite-amalgamation-2.3.1 - $ ./configure # May need to modified, see notes below. + $ ./configure # May need to be modified, see notes below. $ make $ sudo make install - $ cd .... _spatialite + $ cd .. + +.. _spatialite_tools: Finally, do the same for the SpatiaLite tools:: $ cd spatialite-tools-2.3.1 - $ ./configure # May need to modified, see notes below. + $ ./configure # May need to be modified, see notes below. $ make $ sudo make install $ cd .. From 9451d8d558e6dafa4e270c33608a291610ccf77d Mon Sep 17 00:00:00 2001 From: Matt Austin <mail@mattaustin.me.uk> Date: Thu, 12 Sep 2013 21:27:35 +0800 Subject: [PATCH 47/58] Fixed #21095 -- Documented new requirement for dates lookups. Day, month, and week_day lookups now require time zone definitions in the database. --- docs/ref/models/querysets.txt | 9 ++++++--- docs/releases/1.6.txt | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index c62525cb72..bb9f303571 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -2274,7 +2274,8 @@ SQL equivalent:: (The exact SQL syntax varies for each database engine.) When :setting:`USE_TZ` is ``True``, datetime fields are converted to the -current time zone before filtering. +current time zone before filtering. This requires :ref:`time zone definitions +in the database <database-time-zone-definitions>`. .. fieldlookup:: day @@ -2297,7 +2298,8 @@ Note this will match any record with a pub_date on the third day of the month, such as January 3, July 3, etc. When :setting:`USE_TZ` is ``True``, datetime fields are converted to the -current time zone before filtering. +current time zone before filtering. This requires :ref:`time zone definitions +in the database <database-time-zone-definitions>`. .. fieldlookup:: week_day @@ -2321,7 +2323,8 @@ Note this will match any record with a ``pub_date`` that falls on a Monday (day are indexed with day 1 being Sunday and day 7 being Saturday. When :setting:`USE_TZ` is ``True``, datetime fields are converted to the -current time zone before filtering. +current time zone before filtering. This requires :ref:`time zone definitions +in the database <database-time-zone-definitions>`. .. fieldlookup:: hour diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 27c7482535..a7448e0da1 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -435,6 +435,21 @@ but will not be removed from Django until version 1.8. .. _recommendations in the Python documentation: http://docs.python.org/2/library/doctest.html#unittest-api +Time zone-aware ``day``, ``month``, and ``week_day`` lookups +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django 1.6 introduces time zone support for :lookup:`day`, :lookup:`month`, +and :lookup:`week_day` lookups when :setting:`USE_TZ` is ``True``. These +lookups were previously performed in UTC regardless of the current time zone. + +This requires :ref:`time zone definitions in the database +<database-time-zone-definitions>`. If you're using SQLite, you must install +pytz_. If you're using MySQL, you must install pytz_ and load the time zone +tables with `mysql_tzinfo_to_sql`_. + +.. _pytz: http://pytz.sourceforge.net/ +.. _mysql_tzinfo_to_sql: http://dev.mysql.com/doc/refman/5.5/en/mysql-tzinfo-to-sql.html + Addition of ``QuerySet.datetimes()`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From ad6fcdb8d24d84a130f847f45b51d7e892ccca08 Mon Sep 17 00:00:00 2001 From: Daniel Sokolowski <daniel.sokolowski@danols.com> Date: Thu, 12 Sep 2013 10:04:28 -0400 Subject: [PATCH 48/58] Fixed #20844 -- Made AdminEmailHandler respect LOGGING 'formatter' setting. --- django/utils/log.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/django/utils/log.py b/django/utils/log.py index f1af7a249e..e7044741f0 100644 --- a/django/utils/log.py +++ b/django/utils/log.py @@ -84,24 +84,22 @@ class AdminEmailHandler(logging.Handler): record.getMessage() ) filter = get_exception_reporter_filter(request) - request_repr = filter.get_request_repr(request) + request_repr = '\n{0}'.format(filter.get_request_repr(request)) except Exception: subject = '%s: %s' % ( record.levelname, record.getMessage() ) request = None - request_repr = "Request repr() unavailable." + request_repr = "unavailable" subject = self.format_subject(subject) if record.exc_info: exc_info = record.exc_info - stack_trace = '\n'.join(traceback.format_exception(*record.exc_info)) else: exc_info = (None, record.getMessage(), None) - stack_trace = 'No stack trace available' - message = "%s\n\n%s" % (stack_trace, request_repr) + message = "%s\n\nRequest repr(): %s" % (self.format(record), request_repr) reporter = ExceptionReporter(request, is_email=True, *exc_info) html_message = reporter.get_traceback_html() if self.include_html else None mail.mail_admins(subject, message, fail_silently=True, From c1ec08998d1b690855d5e69a1f4d9d2f01d44ae6 Mon Sep 17 00:00:00 2001 From: e0ne <e0ne@e0ne.info> Date: Fri, 13 Sep 2013 08:08:34 -0400 Subject: [PATCH 49/58] Fixed #12288 -- Validated that app names in INSTALLED_APPS are unique --- django/conf/__init__.py | 5 +++-- tests/settings_tests/tests.py | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/django/conf/__init__.py b/django/conf/__init__.py index 199c34fb55..6266ca2394 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -109,8 +109,9 @@ class BaseSettings(object): "to a tuple, not a string.") elif name == "INSTALLED_APPS": value = list(value) # force evaluation of generators on Python 3 - if len(value) != len(set(value)): - raise ImproperlyConfigured("The INSTALLED_APPS setting must contain unique values.") + apps = [s.split('.')[-1] for s in value] + if len(value) != len(set(apps)): + raise ImproperlyConfigured("The INSTALLED_APPS setting must contain unique app names.") object.__setattr__(self, name, value) diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py index a9503358a2..0ffb56bb82 100644 --- a/tests/settings_tests/tests.py +++ b/tests/settings_tests/tests.py @@ -241,11 +241,14 @@ class UniqueSettingsTests(TestCase): def test_unique(self): """ An ImproperlyConfigured exception is raised if the INSTALLED_APPS contains - any duplicate strings. + any duplicate appication names. """ with self.assertRaises(ImproperlyConfigured): self.settings_module.INSTALLED_APPS = ("myApp1", "myApp1", "myApp2", "myApp3") + with self.assertRaises(ImproperlyConfigured): + self.settings_module.INSTALLED_APPS = ("package1.myApp1", "package2.myApp1") + class TrailingSlashURLTests(TestCase): """ From 990ce9aab97802ab7da629be1b0b27ab4bcc8578 Mon Sep 17 00:00:00 2001 From: Kevin Christopher Henry <k@severian.com> Date: Thu, 12 Sep 2013 19:01:47 -0400 Subject: [PATCH 50/58] Documentation -- added instructions on working with pull requests Since non-core contributors are asked to review patches, instructions on working with pull requests were added to the Working with Git and GitHub page (based on the existing instructions in the core committers page). --- .../contributing/committing-code.txt | 2 ++ .../writing-code/working-with-git.txt | 26 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/internals/contributing/committing-code.txt b/docs/internals/contributing/committing-code.txt index bc2f97a485..b81bdd2e04 100644 --- a/docs/internals/contributing/committing-code.txt +++ b/docs/internals/contributing/committing-code.txt @@ -34,6 +34,8 @@ Decisions on new committers will follow the process explained in existing committer privately. Public requests for commit access are potential flame-war starters, and will simply be ignored. +.. _handling-pull-requests: + Handling pull requests ---------------------- diff --git a/docs/internals/contributing/writing-code/working-with-git.txt b/docs/internals/contributing/writing-code/working-with-git.txt index 32fc459e70..65613efcdb 100644 --- a/docs/internals/contributing/writing-code/working-with-git.txt +++ b/docs/internals/contributing/writing-code/working-with-git.txt @@ -212,7 +212,7 @@ This way your branch will contain only commits related to its topic, which makes squashing easier. After review ------------- +~~~~~~~~~~~~ It is unusual to get any non-trivial amount of code into core without changes requested by reviewers. In this case, it is often a good idea to add the @@ -225,7 +225,8 @@ commits, you would run:: git rebase -i HEAD~2 -Squash the second commit into the first. Write a commit message along the lines of:: +Squash the second commit into the first. Write a commit message along the lines +of:: Made changes asked in review by <reviewer> @@ -239,8 +240,25 @@ the public commits during the rebase, you should not need to force-push:: Your pull request should now contain the new commit too. -Note that the committer is likely to squash the review commit into the previous commit -when committing the code. +Note that the committer is likely to squash the review commit into the previous +commit when committing the code. + +Working on a patch +------------------ + +One of the ways that developers can contribute to Django is by reviewing +patches. Those patches will typically exist as pull requests on GitHub and +can be easily integrated into your local repository:: + + git checkout -b pull_xxxxx upstream/master + curl https://github.com/django/django/pull/xxxxx.patch | git am + +This will create a new branch and then apply the changes from the pull request +to it. At this point you can run the tests or do anything else you need to +do to investigate the quality of the patch. + +For more detail on working with pull requests see the +:ref:`guidelines for committers <handling-pull-requests>`. Summary ------- From e4aab1bb8db24e226891556f072968625a68abdf Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Fri, 13 Sep 2013 09:28:38 -0400 Subject: [PATCH 51/58] Fixed #21094 -- Updated reuseable apps tutorial to use pip for installation. Thanks ylb415 at gmail.com for the suggestion. --- docs/intro/reusable-apps.txt | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/docs/intro/reusable-apps.txt b/docs/intro/reusable-apps.txt index f163ff8a06..e42d8bcb3e 100644 --- a/docs/intro/reusable-apps.txt +++ b/docs/intro/reusable-apps.txt @@ -102,7 +102,7 @@ Installing some prerequisites The current state of Python packaging is a bit muddled with various tools. For this tutorial, we're going to use distribute_ to build our package. It's a community-maintained fork of the older ``setuptools`` project. We'll also be -using `pip`_ to uninstall it after we're finished. You should install these +using `pip`_ to install and uninstall it. You should install these two packages now. If you need help, you can refer to :ref:`how to install Django with pip<installing-official-release>`. You can install ``distribute`` the same way. @@ -262,28 +262,18 @@ working. We'll now fix this by installing our new ``django-polls`` package. tools that run as that user, so ``virtualenv`` is a more robust solution (see below). -1. Inside ``django-polls/dist``, untar the new package - ``django-polls-0.1.tar.gz`` (e.g. ``tar xzvf django-polls-0.1.tar.gz``). If - you're using Windows, you can download the command-line tool bsdtar_ to do - this, or you can use a GUI-based tool such as 7-zip_. +1. To install the package, use pip (you already :ref:`installed it + <installing-reusable-apps-prerequisites>`, right?):: -2. Change into the directory created in step 1 (e.g. ``cd django-polls-0.1``). + pip install --user django-polls/dist/django-polls-0.1.tar.gz -3. If you're using GNU/Linux, Mac OS X or some other flavor of Unix, enter the - command ``python setup.py install --user`` at the shell prompt. If you're - using Windows, start up a command shell and run the command - ``setup.py install --user``. - - With luck, your Django project should now work correctly again. Run the +2. With luck, your Django project should now work correctly again. Run the server again to confirm this. -4. To uninstall the package, use pip (you already :ref:`installed it - <installing-reusable-apps-prerequisites>`, right?):: +3. To uninstall the package, use pip:: pip uninstall django-polls -.. _bsdtar: http://gnuwin32.sourceforge.net/packages/bsdtar.htm -.. _7-zip: http://www.7-zip.org/ .. _pip: http://pypi.python.org/pypi/pip Publishing your app From ec89e1725a29076d37b36f85de983b8a7cf7c329 Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Fri, 13 Sep 2013 09:34:12 -0400 Subject: [PATCH 52/58] Fixed #21100 -- Noted that Create/UpdateViews.fields is new in 1.6 Thanks AndrewIngram for the suggestion. --- docs/releases/1.6.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index a7448e0da1..34dc307527 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -1061,12 +1061,12 @@ security problem described in the section above, because they can automatically create a ``ModelForm`` that uses all fields for a model. For this reason, if you use these views for editing models, you must also supply -the ``fields`` attribute, which is a list of model fields and works in the same -way as the :class:`~django.forms.ModelForm` ``Meta.fields`` attribute. Alternatively, -you can set set the ``form_class`` attribute to a ``ModelForm`` that explicitly -defines the fields to be used. Defining an ``UpdateView`` or ``CreateView`` -subclass to be used with a model but without an explicit list of fields is -deprecated. +the ``fields`` attribute (new in Django 1.6), which is a list of model fields +and works in the same way as the :class:`~django.forms.ModelForm` +``Meta.fields`` attribute. Alternatively, you can set set the ``form_class`` +attribute to a ``ModelForm`` that explicitly defines the fields to be used. +Defining an ``UpdateView`` or ``CreateView`` subclass to be used with a model +but without an explicit list of fields is deprecated. .. _m2m-help_text-deprecation: From 39b49fd33970720b973498d2c8275ced129d3a7a Mon Sep 17 00:00:00 2001 From: Goetz <goetz.buerkle@gmail.com> Date: Fri, 13 Sep 2013 16:45:42 +0100 Subject: [PATCH 53/58] Fixed #21101 -- Updated urlize documentation to mention email addresses --- docs/ref/templates/builtins.txt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 9d096e44b3..cc1ca46e9f 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -2226,7 +2226,7 @@ If ``value`` is ``"http://www.example.org/"``, the output will be urlize ^^^^^^ -Converts URLs in text into clickable links. +Converts URLs and email addresses in text into clickable links. This template tag works on links prefixed with ``http://``, ``https://``, or ``www.``. For example, ``http://goo.gl/aia1t`` will get converted but @@ -2250,6 +2250,11 @@ If ``value`` is ``"Check out www.djangoproject.com"``, the output will be ``"Check out <a href="http://www.djangoproject.com" rel="nofollow">www.djangoproject.com</a>"``. +In addition to web links, ``urlize`` also converts email addresses into +``mailto:`` links. If ``value`` is +``"Send questions to foo@example.com"``, the output will be +``"Send questions to <a href="mailto:foo@example.com">foo@example</a>"``. + The ``urlize`` filter also takes an optional parameter ``autoescape``. If ``autoescape`` is ``True``, the link text and URLs will be escaped using Django's built-in :tfilter:`escape` filter. The default value for @@ -2265,7 +2270,7 @@ Django's built-in :tfilter:`escape` filter. The default value for urlizetrunc ^^^^^^^^^^^ -Converts URLs into clickable links just like urlize_, but truncates URLs +Converts URLs and email addresses into clickable links just like urlize_, but truncates URLs longer than the given character limit. **Argument:** Number of characters that link text should be truncated to, From 6feb75129f42fdac751d0b79a2a005b4c0ad5c2d Mon Sep 17 00:00:00 2001 From: Juan Catalano <catalanojuan@gmail.com> Date: Fri, 6 Sep 2013 20:23:25 -0300 Subject: [PATCH 54/58] Fixed #21060 -- Refactored admin's autodiscover method to make it reusable. We want to be able to use it for instance for discovering `tasks.py` modules inside the INSTALLED_APPS. This commit therefore moves the logic to `autodiscover_modules` method in django.utils.module_loading. --- django/contrib/admin/__init__.py | 31 +-------------- django/utils/module_loading.py | 38 +++++++++++++++++++ tests/utils_tests/test_module/__init__.py | 3 ++ .../test_module/another_bad_module.py | 8 ++++ .../test_module/another_good_module.py | 1 + tests/utils_tests/test_module_loading.py | 31 ++++++++++++++- 6 files changed, 82 insertions(+), 30 deletions(-) create mode 100644 tests/utils_tests/test_module/another_bad_module.py create mode 100644 tests/utils_tests/test_module/another_good_module.py diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py index ef2e87407a..3975993679 100644 --- a/django/contrib/admin/__init__.py +++ b/django/contrib/admin/__init__.py @@ -7,35 +7,8 @@ from django.contrib.admin.sites import AdminSite, site from django.contrib.admin.filters import (ListFilter, SimpleListFilter, FieldListFilter, BooleanFieldListFilter, RelatedFieldListFilter, ChoicesFieldListFilter, DateFieldListFilter, AllValuesFieldListFilter) +from django.utils.module_loading import autodiscover_modules def autodiscover(): - """ - Auto-discover INSTALLED_APPS admin.py modules and fail silently when - not present. This forces an import on them to register any admin bits they - may want. - """ - - import copy - from importlib import import_module - from django.conf import settings - from django.utils.module_loading import module_has_submodule - - for app in settings.INSTALLED_APPS: - mod = import_module(app) - # Attempt to import the app's admin module. - try: - before_import_registry = copy.copy(site._registry) - import_module('%s.admin' % app) - except: - # Reset the model registry to the state before the last import as - # this import will have to reoccur on the next request and this - # could raise NotRegistered and AlreadyRegistered exceptions - # (see #8245). - site._registry = before_import_registry - - # Decide whether to bubble up this error. If the app just - # doesn't have an admin module, we can ignore the error - # attempting to import it, otherwise we want it to bubble up. - if module_has_submodule(mod, 'admin'): - raise + autodiscover_modules('admin', register_to=site) diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py index 9c8ea98d50..8868a2d3ab 100644 --- a/django/utils/module_loading.py +++ b/django/utils/module_loading.py @@ -1,5 +1,6 @@ from __future__ import absolute_import # Avoid importing `importlib` from this package. +import copy import imp from importlib import import_module import os @@ -34,6 +35,43 @@ def import_by_path(dotted_path, error_prefix=''): return attr +def autodiscover_modules(*args, **kwargs): + """ + Auto-discover INSTALLED_APPS modules and fail silently when + not present. This forces an import on them to register any admin bits they + may want. + + You may provide a register_to keyword parameter as a way to access a + registry. This register_to object must have a _registry instance variable + to access it. + """ + from django.conf import settings + + register_to = kwargs.get('register_to') + for app in settings.INSTALLED_APPS: + mod = import_module(app) + # Attempt to import the app's module. + try: + if register_to: + before_import_registry = copy.copy(register_to._registry) + + for module_to_search in args: + import_module('%s.%s' % (app, module_to_search)) + except: + # Reset the model registry to the state before the last import as + # this import will have to reoccur on the next request and this + # could raise NotRegistered and AlreadyRegistered exceptions + # (see #8245). + if register_to: + register_to._registry = before_import_registry + + # Decide whether to bubble up this error. If the app just + # doesn't have an admin module, we can ignore the error + # attempting to import it, otherwise we want it to bubble up. + if module_has_submodule(mod, module_to_search): + raise + + def module_has_submodule(package, module_name): """See if 'module' is in 'package'.""" name = ".".join([package.__name__, module_name]) diff --git a/tests/utils_tests/test_module/__init__.py b/tests/utils_tests/test_module/__init__.py index e69de29bb2..8f33921eb6 100644 --- a/tests/utils_tests/test_module/__init__.py +++ b/tests/utils_tests/test_module/__init__.py @@ -0,0 +1,3 @@ +class SiteMock(object): + _registry = {} +site = SiteMock() diff --git a/tests/utils_tests/test_module/another_bad_module.py b/tests/utils_tests/test_module/another_bad_module.py new file mode 100644 index 0000000000..eac25c4aa5 --- /dev/null +++ b/tests/utils_tests/test_module/another_bad_module.py @@ -0,0 +1,8 @@ +from . import site +content = 'Another Bad Module' + +site._registry.update({ + 'foo': 'bar', +}) + +raise Exception('Some random exception.') diff --git a/tests/utils_tests/test_module/another_good_module.py b/tests/utils_tests/test_module/another_good_module.py new file mode 100644 index 0000000000..ed4b2d34a2 --- /dev/null +++ b/tests/utils_tests/test_module/another_good_module.py @@ -0,0 +1 @@ +content = 'Another Good Module' diff --git a/tests/utils_tests/test_module_loading.py b/tests/utils_tests/test_module_loading.py index 2423215778..d808ab8783 100644 --- a/tests/utils_tests/test_module_loading.py +++ b/tests/utils_tests/test_module_loading.py @@ -6,7 +6,10 @@ import unittest from zipimport import zipimporter from django.core.exceptions import ImproperlyConfigured -from django.utils.module_loading import import_by_path, module_has_submodule +from django.test import SimpleTestCase +from django.test.utils import override_settings +from django.utils import six +from django.utils.module_loading import autodiscover_modules, import_by_path, module_has_submodule from django.utils._os import upath @@ -130,6 +133,32 @@ class ModuleImportTestCase(unittest.TestCase): self.assertIsNotNone(traceback.tb_next.tb_next, 'Should have more than the calling frame in the traceback.') +@override_settings(INSTALLED_APPS=('utils_tests.test_module',)) +class AutodiscoverModulesTestCase(SimpleTestCase): + + def test_autodiscover_modules_found(self): + autodiscover_modules('good_module') + + def test_autodiscover_modules_not_found(self): + autodiscover_modules('missing_module') + + def test_autodiscover_modules_found_but_bad_module(self): + with six.assertRaisesRegex(self, ImportError, "No module named '?a_package_name_that_does_not_exist'?"): + autodiscover_modules('bad_module') + + def test_autodiscover_modules_several_one_bad_module(self): + with six.assertRaisesRegex(self, ImportError, "No module named '?a_package_name_that_does_not_exist'?"): + autodiscover_modules('good_module', 'bad_module') + + def test_autodiscover_modules_several_found(self): + autodiscover_modules('good_module', 'another_good_module') + + def test_validate_registry_keeps_intact(self): + from .test_module import site + with six.assertRaisesRegex(self, Exception, "Some random exception."): + autodiscover_modules('another_bad_module', register_to=site) + self.assertEqual(site._registry, {}) + class ProxyFinder(object): def __init__(self): From c89d80e2cc9bf1f401aa3af4047bdc6f3dc5bfa4 Mon Sep 17 00:00:00 2001 From: Michael Manfre <mmanfre@gmail.com> Date: Thu, 12 Sep 2013 10:03:29 -0400 Subject: [PATCH 55/58] Fixed #21097 - Added DatabaseFeature.can_introspect_autofield --- django/core/management/commands/inspectdb.py | 7 +++++-- django/db/backends/__init__.py | 3 +++ tests/inspectdb/tests.py | 3 ++- tests/introspection/tests.py | 5 +++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py index b4c116acab..89549dc3c1 100644 --- a/django/core/management/commands/inspectdb.py +++ b/django/core/management/commands/inspectdb.py @@ -104,8 +104,11 @@ class Command(NoArgsCommand): # Don't output 'id = meta.AutoField(primary_key=True)', because # that's assumed if it doesn't exist. - if att_name == 'id' and field_type == 'AutoField(' and extra_params == {'primary_key': True}: - continue + if att_name == 'id' and extra_params == {'primary_key': True}: + if field_type == 'AutoField(': + continue + elif field_type == 'IntegerField(' and not connection.features.can_introspect_autofield: + comment_notes.append('AutoField?') # Add 'null' and 'blank', if the 'null_ok' flag was present in the # table description. diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index d22fea5ec2..44024c5349 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -627,6 +627,9 @@ class BaseDatabaseFeatures(object): # which can't do it for MyISAM tables can_introspect_foreign_keys = True + # Can the backend introspect an AutoField, instead of an IntegerField? + can_introspect_autofield = False + # Support for the DISTINCT ON clause can_distinct_on_fields = False diff --git a/tests/inspectdb/tests.py b/tests/inspectdb/tests.py index d6f6e478e4..302db29e5d 100644 --- a/tests/inspectdb/tests.py +++ b/tests/inspectdb/tests.py @@ -42,7 +42,8 @@ class InspectDBTestCase(TestCase): out_def = re.search(r'^\s*%s = (models.*)$' % name, output, re.MULTILINE).groups()[0] self.assertEqual(definition, out_def) - assertFieldType('id', "models.IntegerField(primary_key=True)") + if not connection.features.can_introspect_autofield: + assertFieldType('id', "models.IntegerField(primary_key=True) # AutoField?") assertFieldType('big_int_field', "models.BigIntegerField()") if connection.vendor == 'mysql': # No native boolean type on MySQL diff --git a/tests/introspection/tests.py b/tests/introspection/tests.py index 6f38439945..8ec3d39903 100644 --- a/tests/introspection/tests.py +++ b/tests/introspection/tests.py @@ -66,8 +66,9 @@ class IntrospectionTests(TestCase): # field type on MySQL self.assertEqual( [datatype(r[1], r) for r in desc], - ['IntegerField', 'CharField', 'CharField', 'CharField', - 'BigIntegerField', 'BinaryField' if connection.vendor != 'mysql' else 'TextField'] + ['AutoField' if connection.features.can_introspect_autofield else 'IntegerField', + 'CharField', 'CharField', 'CharField', 'BigIntegerField', + 'BinaryField' if connection.vendor != 'mysql' else 'TextField'] ) # The following test fails on Oracle due to #17202 (can't correctly From 74b91b3888383fca28dea00e0e1ffb5aecec7c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= <anssi.kaariainen@thl.fi> Date: Sat, 14 Sep 2013 10:33:12 +0300 Subject: [PATCH 56/58] Added tests for double-pickling a QuerySet Refs #21102. --- tests/queryset_pickle/tests.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/queryset_pickle/tests.py b/tests/queryset_pickle/tests.py index 384073ad56..602739fa54 100644 --- a/tests/queryset_pickle/tests.py +++ b/tests/queryset_pickle/tests.py @@ -94,3 +94,15 @@ class PickleabilityTestCase(TestCase): def test_specialized_queryset(self): self.assert_pickles(Happening.objects.values('name')) self.assert_pickles(Happening.objects.values('name').dates('when', 'year')) + + def test_pickle_prefetch_related_idempotence(self): + g = Group.objects.create(name='foo') + groups = Group.objects.prefetch_related('event_set') + + # First pickling + groups = pickle.loads(pickle.dumps(groups)) + self.assertQuerysetEqual(groups, [g], lambda x: x) + + # Second pickling + groups = pickle.loads(pickle.dumps(groups)) + self.assertQuerysetEqual(groups, [g], lambda x: x) From 886bb9d8780303b4c8f45c55e0ac0a6b644b73af Mon Sep 17 00:00:00 2001 From: Tim Graham <timograham@gmail.com> Date: Sat, 14 Sep 2013 07:19:32 -0400 Subject: [PATCH 57/58] Revert "Fixed #12288 -- Validated that app names in INSTALLED_APPS are unique" This reverts commit c1ec08998d1b690855d5e69a1f4d9d2f01d44ae6. There are backwards compatability concerns with this. --- django/conf/__init__.py | 5 ++--- tests/settings_tests/tests.py | 5 +---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/django/conf/__init__.py b/django/conf/__init__.py index 6266ca2394..199c34fb55 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -109,9 +109,8 @@ class BaseSettings(object): "to a tuple, not a string.") elif name == "INSTALLED_APPS": value = list(value) # force evaluation of generators on Python 3 - apps = [s.split('.')[-1] for s in value] - if len(value) != len(set(apps)): - raise ImproperlyConfigured("The INSTALLED_APPS setting must contain unique app names.") + if len(value) != len(set(value)): + raise ImproperlyConfigured("The INSTALLED_APPS setting must contain unique values.") object.__setattr__(self, name, value) diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py index 0ffb56bb82..a9503358a2 100644 --- a/tests/settings_tests/tests.py +++ b/tests/settings_tests/tests.py @@ -241,14 +241,11 @@ class UniqueSettingsTests(TestCase): def test_unique(self): """ An ImproperlyConfigured exception is raised if the INSTALLED_APPS contains - any duplicate appication names. + any duplicate strings. """ with self.assertRaises(ImproperlyConfigured): self.settings_module.INSTALLED_APPS = ("myApp1", "myApp1", "myApp2", "myApp3") - with self.assertRaises(ImproperlyConfigured): - self.settings_module.INSTALLED_APPS = ("package1.myApp1", "package2.myApp1") - class TrailingSlashURLTests(TestCase): """ From ff723d894d9272ea721d1996432ffc806c2b8180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= <akaariai@gmail.com> Date: Wed, 21 Aug 2013 14:25:19 +0300 Subject: [PATCH 58/58] Fixed #20950 -- Instantiate OrderedDict() only when needed The use of OrderedDict (even an empty one) was surprisingly slow. By initializing OrderedDict only when needed it is possible to save non-trivial amount of computing time (Model.save() is around 30% faster for example). This commit targetted sql.Query only, there are likely other places which could use similar optimizations. --- django/db/models/sql/compiler.py | 4 +- django/db/models/sql/query.py | 65 ++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 1c6e80b538..c42149e45a 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -391,7 +391,7 @@ class SQLCompiler(object): if not distinct or elt in select_aliases: result.append('%s %s' % (elt, order)) group_by.append((elt, [])) - elif get_order_dir(field)[0] not in self.query.extra: + elif not self.query._extra or get_order_dir(field)[0] not in self.query._extra: # 'col' is of the form 'field' or 'field1__field2' or # '-field1__field2__field', etc. for table, cols, order in self.find_ordering_name(field, @@ -987,7 +987,7 @@ class SQLUpdateCompiler(SQLCompiler): # We need to use a sub-select in the where clause to filter on things # from other tables. query = self.query.clone(klass=Query) - query.extra = {} + query._extra = {} query.select = [] query.add_fields([query.get_meta().pk.name]) # Recheck the count - it is possible that fiddling with the select diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 93a0b52330..9eea55e98e 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -143,7 +143,10 @@ class Query(object): self.select_related = False # SQL aggregate-related attributes - self.aggregates = OrderedDict() # Maps alias -> SQL aggregate function + # The _aggregates will be an OrderedDict when used. Due to the cost + # of creating OrderedDict this attribute is created lazily (in + # self.aggregates property). + self._aggregates = None # Maps alias -> SQL aggregate function self.aggregate_select_mask = None self._aggregate_select_cache = None @@ -153,7 +156,9 @@ class Query(object): # These are for extensions. The contents are more or less appended # verbatim to the appropriate clause. - self.extra = OrderedDict() # Maps col_alias -> (col_sql, params). + # The _extra attribute is an OrderedDict, lazily created similarly to + # .aggregates + self._extra = None # Maps col_alias -> (col_sql, params). self.extra_select_mask = None self._extra_select_cache = None @@ -165,6 +170,18 @@ class Query(object): # load. self.deferred_loading = (set(), True) + @property + def extra(self): + if self._extra is None: + self._extra = OrderedDict() + return self._extra + + @property + def aggregates(self): + if self._aggregates is None: + self._aggregates = OrderedDict() + return self._aggregates + def __str__(self): """ Returns the query as a string of SQL with the parameter values @@ -245,7 +262,7 @@ class Query(object): obj.select_for_update_nowait = self.select_for_update_nowait obj.select_related = self.select_related obj.related_select_cols = [] - obj.aggregates = self.aggregates.copy() + obj._aggregates = self._aggregates.copy() if self._aggregates is not None else None if self.aggregate_select_mask is None: obj.aggregate_select_mask = None else: @@ -257,7 +274,7 @@ class Query(object): # used. obj._aggregate_select_cache = None obj.max_depth = self.max_depth - obj.extra = self.extra.copy() + obj._extra = self._extra.copy() if self._extra is not None else None if self.extra_select_mask is None: obj.extra_select_mask = None else: @@ -344,7 +361,7 @@ class Query(object): # and move them to the outer AggregateQuery. for alias, aggregate in self.aggregate_select.items(): if aggregate.is_summary: - query.aggregate_select[alias] = aggregate.relabeled_clone(relabels) + query.aggregates[alias] = aggregate.relabeled_clone(relabels) del obj.aggregate_select[alias] try: @@ -358,7 +375,7 @@ class Query(object): query = self self.select = [] self.default_cols = False - self.extra = {} + self._extra = {} self.remove_inherited_models() query.clear_ordering(True) @@ -527,7 +544,7 @@ class Query(object): # It would be nice to be able to handle this, but the queries don't # really make sense (or return consistent value sets). Not worth # the extra complexity when you can write a real query instead. - if self.extra and rhs.extra: + if self._extra and rhs._extra: raise ValueError("When merging querysets using 'or', you " "cannot have extra(select=...) on both sides.") self.extra.update(rhs.extra) @@ -756,8 +773,9 @@ class Query(object): self.group_by = [relabel_column(col) for col in self.group_by] self.select = [SelectInfo(relabel_column(s.col), s.field) for s in self.select] - self.aggregates = OrderedDict( - (key, relabel_column(col)) for key, col in self.aggregates.items()) + if self._aggregates: + self._aggregates = OrderedDict( + (key, relabel_column(col)) for key, col in self._aggregates.items()) # 2. Rename the alias in the internal table/alias datastructures. for ident, aliases in self.join_map.items(): @@ -967,7 +985,7 @@ class Query(object): """ opts = model._meta field_list = aggregate.lookup.split(LOOKUP_SEP) - if len(field_list) == 1 and aggregate.lookup in self.aggregates: + if len(field_list) == 1 and self._aggregates and aggregate.lookup in self.aggregates: # Aggregate is over an annotation field_name = field_list[0] col = field_name @@ -1049,7 +1067,7 @@ class Query(object): lookup_parts = lookup.split(LOOKUP_SEP) num_parts = len(lookup_parts) if (len(lookup_parts) > 1 and lookup_parts[-1] in self.query_terms - and lookup not in self.aggregates): + and (not self._aggregates or lookup not in self._aggregates)): # Traverse the lookup query to distinguish related fields from # lookup types. lookup_model = self.model @@ -1108,10 +1126,11 @@ class Query(object): value, lookup_type = self.prepare_lookup_value(value, lookup_type, can_reuse) clause = self.where_class() - for alias, aggregate in self.aggregates.items(): - if alias in (parts[0], LOOKUP_SEP.join(parts)): - clause.add((aggregate, lookup_type, value), AND) - return clause + if self._aggregates: + for alias, aggregate in self.aggregates.items(): + if alias in (parts[0], LOOKUP_SEP.join(parts)): + clause.add((aggregate, lookup_type, value), AND) + return clause opts = self.get_meta() alias = self.get_initial_alias() @@ -1170,6 +1189,8 @@ class Query(object): Returns whether or not all elements of this q_object need to be put together in the HAVING clause. """ + if not self._aggregates: + return False if not isinstance(obj, Node): return (refs_aggregate(obj[0].split(LOOKUP_SEP), self.aggregates) or (hasattr(obj[1], 'contains_aggregate') @@ -1632,7 +1653,7 @@ class Query(object): # Set only aggregate to be the count column. # Clear out the select cache to reflect the new unmasked aggregates. - self.aggregates = {None: count} + self._aggregates = {None: count} self.set_aggregate_mask(None) self.group_by = None @@ -1781,7 +1802,8 @@ class Query(object): self.extra_select_mask = set(names) self._extra_select_cache = None - def _aggregate_select(self): + @property + def aggregate_select(self): """The OrderedDict of aggregate columns that are not masked, and should be used in the SELECT clause. @@ -1789,6 +1811,8 @@ class Query(object): """ if self._aggregate_select_cache is not None: return self._aggregate_select_cache + elif not self._aggregates: + return {} elif self.aggregate_select_mask is not None: self._aggregate_select_cache = OrderedDict( (k, v) for k, v in self.aggregates.items() @@ -1797,11 +1821,13 @@ class Query(object): return self._aggregate_select_cache else: return self.aggregates - aggregate_select = property(_aggregate_select) - def _extra_select(self): + @property + def extra_select(self): if self._extra_select_cache is not None: return self._extra_select_cache + if not self._extra: + return {} elif self.extra_select_mask is not None: self._extra_select_cache = OrderedDict( (k, v) for k, v in self.extra.items() @@ -1810,7 +1836,6 @@ class Query(object): return self._extra_select_cache else: return self.extra - extra_select = property(_extra_select) def trim_start(self, names_with_path): """